Просмотр исходного кода

ref: plugin interface types and contrib guide

Sven 10 месяцев назад
Родитель
Сommit
aac95f8db3
3 измененных файлов с 111 добавлено и 38 удалено
  1. 23 16
      contributing.md
  2. 7 7
      src/interface.ts
  3. 81 15
      src/types.ts

+ 23 - 16
contributing.md

@@ -363,7 +363,7 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 >   
 > The returned properties include:  
 > - `token` - A private token that is used for authenticated function calls and that **should not be persistently stored** beyond the current session
-> - `events` - A nano-events emitter object that allows you to listen for events that are dispatched by BetterYTM  
+> - `events` - A [NanoEmitter](#nanoemitter) instance that allows you to listen for plugin-specific events that are dispatched by BetterYTM.  
 >   To find a list of all events, search for `PluginEventMap` in the file [`src/types.ts`](./src/types.ts)
 > - `info` - The info object that contains all data other plugins will be able to see about your plugin
 > 
@@ -376,26 +376,32 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 >   plugin: {
 >     name: "My cool plugin",                           // required
 >     namespace: "https://github.com/MyUsername",       // required
->     version: [4, 2, 0],                               // required
+>     version: "4.2.0",                                 // required
 >     description: {                                    // required
 >       en_US: "This plugin does cool stuff",           // required
 >       de_DE: "Dieses Plugin macht coole Sachen",
 >       // see all supported locale codes in "assets/locales.json"
 >     },
 >     iconUrl: "https://picsum.photos/128/128",
->     homepage: {
+>     license: {                                    // (optional)
+>       name: "MIT",                                // required
+>       url: "https://opensource.org/licenses/MIT", // required
+>     },
+>     homepage: {                                                 // required
+>       source: "https://github.com/MyUsername/MyCoolBYTMPlugin", // required
 >       other: "https://example.org/MyCoolBYTMPlugin",
->       source: "https://github.com/MyUsername/MyCoolBYTMPlugin",
+>       bug: "https://github.com/MyUsername/MyCoolBYTMPlugin/issues",
 >       greasyfork: "...",
 >       openuserjs: "...",
 >     },
 >   },
->   // the intents (permissions) the plugin needs to be granted
->   // search for "enum PluginIntent" in "src/types.ts" to see all available intent values
->   intents: [ 2, 16 ],
->   contributors: [
+>   // the intents (permissions) the plugin needs to be granted to be able to use certain functions
+>   // search for "enum PluginIntent" in "src/types.ts" to see all available values,
+>   // then sum all of them together to get the final intents number
+>   intents: 18,
+>   contributors: [           // (optional)
 >     {
->       name: "MyUsername", // required
+>       name: "MyUsername",   // required
 >       homepage: "https://github.com/MyUsername",
 >       email: "[email protected]",
 >     },
@@ -407,7 +413,7 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 >   ],
 > };
 > 
-> // private token that should not be stored persistently (in memory like this should be enough)
+> // private token for authenticated function calls (don't store this persistently, as your plugin gets a new one every session!)
 > let authToken: string | undefined;
 > 
 > // since some function calls require the token, this function can be called to get it once the plugin is fully registered
@@ -444,19 +450,20 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 >   
 > Arguments:
 > - `token` - The private token that was returned when the plugin was registered (if not provided, the function will always return `undefined`)
-> - `name` - The 'name' property of the plugin
-> - `namespace` - The 'namespace' property of the plugin
-> OR:
-> - `pluginDef` - A plugin definition object containing at least the `plugin.name` and `plugin.namespace` properties
+> - either:
+>   - `name` - The "name" property of the plugin
+>   - `namespace` - The "namespace" property of the plugin
+> - or:
+>   - `pluginDef` - A plugin definition object containing at least the `plugin.name` and `plugin.namespace` properties
 >   
-> The function will return `undefined` if the plugin is not registered.  
+> The function will return `undefined` if the plugin is not registered or the token is invalid.  
 > The type of the returned object can be found by searching for `type PluginInfo` in the file [`src/types.ts`](./src/types.ts)
 > 
 > <details><summary><b>Example <i>(click to expand)</i></b></summary>
 > 
 > ```ts
 > unsafeWindow.addEventListener("bytm:pluginsRegistered", () => {
->   const pluginInfo = unsafeWindow.BYTM.getPluginInfo("My cool plugin", "https://github.com/MyUsername");
+>   const pluginInfo = unsafeWindow.BYTM.getPluginInfo(myToken, "My cool plugin", "https://github.com/MyUsername");
 >   if(pluginInfo) {
 >     console.log(`The plugin '${pluginInfo.name}' with version '${pluginInfo.version.join(".")}' is loaded`);
 >   }

+ 7 - 7
src/interface.ts

@@ -6,7 +6,7 @@ import { addSelectorListener } from "./observers.js";
 import { getFeatures, setFeatures } from "./config.js";
 import { autoLikeStore, featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong } from "./features/index.js";
 import { allSiteEvents, type SiteEventsMap } from "./siteEvents.js";
-import { LogLevel, type FeatureConfig, type FeatureInfo, type LyricsCacheEntry, type PluginDef, type PluginInfo, type PluginRegisterResult, type PluginDefResolvable, type PluginEventMap, type PluginItem, type BytmObject, type AutoLikeData } from "./types.js";
+import { LogLevel, type FeatureConfig, type FeatureInfo, type LyricsCacheEntry, type PluginDef, type PluginInfo, type PluginRegisterResult, type PluginDefResolvable, type PluginEventMap, type PluginItem, type BytmObject, type AutoLikeData, type InterfaceFunctions } from "./types.js";
 import { BytmDialog, ExImDialog, createCircularBtn, createHotkeyInput, createRipple, createToggleInput, showIconToast, showToast } from "./components/index.js";
 
 const { getUnsafeWindow, randomId } = UserUtils;
@@ -109,7 +109,7 @@ export const allInterfaceEvents = [
  * All functions that can be called on the BYTM interface using `unsafeWindow.BYTM.functionName();` (or `const { functionName } = unsafeWindow.BYTM;`)  
  * If prefixed with /**\/, the function is authenticated and requires a token to be passed as the first argument.
  */
-const globalFuncs = {
+const globalFuncs: InterfaceFunctions = {
   // meta:
   registerPlugin,
   /**/ getPluginInfo,
@@ -406,7 +406,7 @@ export function resolveToken(token: string | undefined): string | undefined {
  * Sets the new locale on the BYTM interface  
  * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
  */
-function setLocaleInterface(token: string | undefined, locale: TrLocale) {
+export function setLocaleInterface(token: string | undefined, locale: TrLocale) {
   const pluginId = resolveToken(token);
   if(pluginId === undefined)
     return;
@@ -418,7 +418,7 @@ function setLocaleInterface(token: string | undefined, locale: TrLocale) {
  * Returns the current feature config, with sensitive values replaced by `undefined`  
  * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
  */
-function getFeaturesInterface(token: string | undefined) {
+export function getFeaturesInterface(token: string | undefined) {
   if(resolveToken(token) === undefined)
     return undefined;
   const features = getFeatures();
@@ -434,7 +434,7 @@ function getFeaturesInterface(token: string | undefined) {
  * Saves the passed feature config synchronously to the in-memory cache and asynchronously to the persistent storage.  
  * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
  */
-function saveFeaturesInterface(token: string | undefined, features: FeatureConfig) {
+export function saveFeaturesInterface(token: string | undefined, features: FeatureConfig) {
   if(resolveToken(token) === undefined)
     return;
   setFeatures(features);
@@ -444,7 +444,7 @@ function saveFeaturesInterface(token: string | undefined, features: FeatureConfi
  * Returns the auto-like data.  
  * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
  */
-function getAutoLikeDataInterface(token: string | undefined) {
+export function getAutoLikeDataInterface(token: string | undefined) {
   if(resolveToken(token) === undefined)
     return;
   return autoLikeStore.getData();
@@ -454,7 +454,7 @@ function getAutoLikeDataInterface(token: string | undefined) {
  * Saves new auto-like data, synchronously to the in-memory cache and asynchronously to the persistent storage.  
  * This is an authenticated function so you must pass the session- and plugin-unique token, retreived at registration.
  */
-function saveAutoLikeDataInterface(token: string | undefined, data: AutoLikeData) {
+export function saveAutoLikeDataInterface(token: string | undefined, data: AutoLikeData) {
   if(resolveToken(token) === undefined)
     return;
   return autoLikeStore.setData(data);

+ 81 - 15
src/types.ts

@@ -3,10 +3,12 @@ import type { scriptInfo } from "./constants.js";
 import type { addSelectorListener } from "./observers.js";
 import type resources from "../assets/resources.json";
 import type locales from "../assets/locales.json";
-import type { getResourceUrl, getSessionId, getVideoTime, TrLocale, t, tp, NanoEmitter } from "./utils/index.js";
-import type { getFeatures, setFeatures } from "./config.js";
+import type { getResourceUrl, getSessionId, getVideoTime, TrLocale, t, tp, NanoEmitter, fetchVideoVotes, onInteraction, getThumbnailUrl, getBestThumbnailUrl, getLocale, hasKey, hasKeyFor } from "./utils/index.js";
 import type { SiteEventsMap } from "./siteEvents.js";
-import type { InterfaceEventsMap } from "./interface.js";
+import type { InterfaceEventsMap, getAutoLikeDataInterface, getFeaturesInterface, getPluginInfo, registerPlugin, saveAutoLikeDataInterface, saveFeaturesInterface, setLocaleInterface } from "./interface.js";
+import type { BytmDialog, ExImDialog, createCircularBtn, createHotkeyInput, createRipple, createToggleInput, showIconToast, showToast } from "./components/index.js";
+import type { fetchLyricsUrlTop, sanitizeArtists, sanitizeSong } from "./features/lyrics.js";
+import type { getLyricsCacheEntry } from "./features/lyricsCache.js";
 
 //#region other
 
@@ -96,7 +98,7 @@ export type VideoVotesObj = {
 
 //#region global
 
-// shim for the BYTM interface properties
+/** All properties of the `unsafeWindow.BYTM` object (also called "plugin interface") */
 export type BytmObject =
   {
     [key: string]: unknown;
@@ -111,8 +113,13 @@ export type BytmObject =
   & InterfaceFunctions
   // others
   & {
+    NanoEmitter: typeof NanoEmitter;
+    BytmDialog: typeof BytmDialog;
+    ExImDialog: typeof ExImDialog;
     // the entire UserUtils library
     UserUtils: typeof import("@sv443-network/userutils");
+    // the entire compare-versions library
+    compareVersions: typeof import("compare-versions");
   };
 
 declare global {
@@ -192,10 +199,10 @@ export type PluginDef = {
     };
     /** Homepage URLs for the plugin */
     homepage: {
-      /** Any other homepage URL */
-      other?: string;
       /** URL to the plugin's source code (i.e. Git repo) - closed source plugins are not officially accepted at the moment. */
       source: string;
+      /** Any other homepage URL */
+      other?: string;
       /** URL to the plugin's bug tracker page, like GitHub issues */
       bug?: string;
       /** URL to the plugin's GreasyFork page */
@@ -205,7 +212,7 @@ export type PluginDef = {
     };
   };
   /** Intents (permissions) BYTM has to grant the plugin for it to work */
-  intents?: Array<PluginIntent>;
+  intents?: number;
   /** Info about the plugin contributors */
   contributors?: Array<{
     /** Name of this contributor */
@@ -219,12 +226,12 @@ export type PluginDef = {
 
 /** All events that are dispatched to plugins individually, including everything in {@linkcode SiteEventsMap} and {@linkcode InterfaceEventsMap} - these don't have a prefix since they can't conflict with other events */
 export type PluginEventMap =
-  // Emitted on each plugin individually:
+  // These are emitted on each plugin individually, with individual data:
   & {
     /** Emitted when the plugin is registered on BYTM's side */
     pluginRegistered: (info: PluginInfo) => void;
   }
-  // Emitted on every plugin simultaneously:
+  // These are emitted on every plugin simultaneously, with the same or similar data:
   & SiteEventsMap
   & InterfaceEventsMap;
 
@@ -237,8 +244,13 @@ export type PluginItem =
 
 /** All functions exposed by the interface on the global `BYTM` object */
 export type InterfaceFunctions = {
-  /** Adds a listener to one of the already present SelectorObserver instances */
-  addSelectorListener: typeof addSelectorListener;
+  // meta:
+  /** Registers a plugin with BYTM. Needed to receive the token for making authenticated function calls. */
+  registerPlugin: typeof registerPlugin;
+  /** 🔒 Checks if the plugin with the given name and namespace is registered and returns an info object about it */
+  getPluginInfo: typeof getPluginInfo;
+
+  // bytm-specific:
   /**
    * Returns the URL of a resource as defined in `assets/resources.json`  
    * There are also some resources like translation files that get added by `tools/post-build.ts`  
@@ -249,20 +261,74 @@ export type InterfaceFunctions = {
   getResourceUrl: typeof getResourceUrl;
   /** Returns the unique session ID for the current tab */
   getSessionId: typeof getSessionId;
+
+  // dom:
+  /** Adds a listener to one of the already present SelectorObserver instances */
+  addSelectorListener: typeof addSelectorListener;
+  /** Registers accessible interaction listeners (click, enter, space) on the provided element */
+  onInteraction: typeof onInteraction;
   /**
    * Returns the current video time (on both YT and YTM)  
    * In case it can't be determined on YT, mouse movement is simulated to bring up the video time  
    * In order for that edge case not to error out, the function would need to be called in response to a user interaction event (e.g. click) due to the strict autoplay policy in browsers
    */
   getVideoTime: typeof getVideoTime;
+  /** Returns the thumbnail URL for the provided video ID and thumbnail quality */
+  getThumbnailUrl: typeof getThumbnailUrl;
+  /** Returns the thumbnail URL with the best quality for the provided video ID */
+  getBestThumbnailUrl: typeof getBestThumbnailUrl;
+
+  // translations:
+  /** 🔒 Sets the locale for all new translations */
+  setLocale: typeof setLocaleInterface;
+  /** Returns the current locale */
+  getLocale: typeof getLocale;
+  /** Returns whether a translation key exists for the set locale */
+  hasKey: typeof hasKey;
+  /** Returns whether a translation key exists for the provided locale */
+  hasKeyFor: typeof hasKeyFor;
   /** Returns the translation for the provided translation key and set locale (check the files in the folder `assets/translations`) */
   t: typeof t;
   /** Returns the translation for the provided translation key, including pluralization identifier and set locale (check the files in the folder `assets/translations`) */
   tp: typeof tp;
-  /** Returns the current feature configuration */
-  getFeatures: typeof getFeatures;
-  /** Overwrites the feature configuration with the provided one */
-  saveFeatures: typeof setFeatures;
+
+  // feature config:
+  /** 🔒 Returns the current feature configuration */
+  getFeatures: typeof getFeaturesInterface;
+  /** 🔒 Overwrites the feature configuration with the provided one */
+  saveFeatures: typeof saveFeaturesInterface;
+
+  // lyrics:
+  /** Sanitizes the provided artist string - this needs to be done before calling other lyrics related functions! */
+  sanitizeArtists: typeof sanitizeArtists;
+  /** Sanitizes the provided song title string - this needs to be done before calling other lyrics related functions! */
+  sanitizeSong: typeof sanitizeSong;
+  /** Fetches the lyrics URL of the top search result for the provided song and artist. Before a request is sent, the cache is checked for a match. */
+  fetchLyricsUrlTop: typeof fetchLyricsUrlTop;
+  /** Returns the lyrics cache entry for the provided song and artist, if there is one. Never sends a request on its own. */
+  getLyricsCacheEntry: typeof getLyricsCacheEntry;
+
+  // auto-like:
+  /** 🔒 Returns the current auto-like data */
+  getAutoLikeData: typeof getAutoLikeDataInterface;
+  /** 🔒 Overwrites the auto-like data */
+  saveAutoLikeData: typeof saveAutoLikeDataInterface;
+  /** Returns the votes for the provided video ID from the ReturnYoutubeDislike API */
+  fetchVideoVotes: typeof fetchVideoVotes;
+
+  // components:
+  /** Creates a new hotkey input component */
+  createHotkeyInput: typeof createHotkeyInput;
+  /** Creates a new toggle input component */
+  createToggleInput: typeof createToggleInput;
+  /** Creates a new circular button component */
+  createCircularBtn: typeof createCircularBtn;
+  /** Creates a new ripple effect on the provided element or creates an empty element that has the effect */
+  createRipple: typeof createRipple;
+  /** Shows a toast with the provided text */
+  showToast: typeof showToast;
+  /** Shows a toast with the provided text and an icon */
+  showIconToast: typeof showIconToast;
 };
 
 //#region feature defs