Skip to content

Publishing, installing and updating

This page follows a package from your hands to the user’s disk. Building and signing are in Packaging; the manifest keys in the full reference.

You distribute a .fgpl file. Users get it onto their machine and open it in freshGram in one of two ways:

  • Settings → Plugins → install from file (file picker for *.fgpl, files up to 64 MiB);
  • open a .fgpl document received in a chat (downloaded files with the .fgpl extension open the install dialog).

Plugins must be enabled in the app first; otherwise the user sees “enable plugins first”. The package is fully validated before the dialog appears — if it is damaged the user gets a toast with the first problem and no dialog.

The updates.feed manifest key is accepted but not used by the current app: there is no automatic update check, and no feed format is implemented. Updates reach users as a newer .fgpl file (Update flow).

Title: Install plugin for a new plugin, Update plugin if the opened file is newer than the installed one, or “already installed” when it is the same version.

From top to bottom:

  1. Header: icon (icon), name, version, author (author.name), description, license, links (homepage, or the author URL if absent; repository, support). Texts are picked for the app language from the localized strings.
  2. Trust line: a red warning “Unofficial plugin. Install it only if you trust its source.” for every package that is not signed with the official freshGram key (this includes your own signed packages and all unsigned ones); “Official plugin” for the official key. Your fingerprint is the publisher id (Packaging → Signing).
  3. Outdated notice if compat.testedWith is older than the running client (higher major, or same major and higher minor): “Made for freshGram X and may not work correctly in this version.”
  4. Version line if the plugin is already installed: “Update from A to B”; for the same version only “Details” and “Close” buttons are offered; for an older file a red “A newer version A is installed; this file has the older B” and the button becomes Install older version.
  5. Command collisions: for each contributes.commands name already used by another plugin, a notice that the user must type /command@plugin.id to reach this one.
  6. Accepts messages from (when contributes.messages.from is set).
  7. Contributes: a list of menus, commands and settings page the plugin adds.
  8. Limits: memory, event time and storage from runtime.limits.
  9. Permissions: see below.
  10. Buttons: Install / Update / Install older version, and Cancel.

Permissions are split into Required (switched on, cannot be turned off) and Optional (toggles), each sorted so the riskiest come first. Every row shows the permission title, then reason (in the app language; the permission id if the reason is empty), and for net.local the port list. Rows are coloured by risk: High, Medium, Low (the level of each permission is in the permission list; messages.read is High for all and Medium for selected).

Below the rows, depending on the manifest:

Control Shown when Behaviour
Chats: all chats / selected chats + chat picker the plugin asks for any chat-related permission If a messages.read request has scope: "selected", “all chats” is disabled. Choosing “selected” with no chats is refused (the host itself would silently drop a selected messages.read that arrives without a chat selection, so the permission would not be granted)
Folder picker the manifest requests fs.folder and it is switched on The user must pick a folder (read-only or read-write according to mode); it cannot be empty, and only valid roots are accepted
Risks text always A list of warnings computed from the set of permissions currently switched on

If the manifest requests no permissions the dialog says so.

After the user presses the button, the host runs these checks in this order (the lock check is first); the first failure aborts with the reason shown in a toast “Cannot install: …”:

Error text Cause
the app is locked the app is locked, or plugins are hidden
damaged package the package fails validation, for example an icon that is not a PNG or is over 256 KiB, or a files map that lists manifest.json
incompatible with this app compat not satisfied (client version, API level, requires, Premium)
unsigned package policy refused unsigned (the app dialog allows it after the user presses Install)
missing dependency a dependencies entry is not satisfied
conflicts with an installed plugin a conflicts rule matches
this version is already installed same version as the installed one
a newer version is installed downgrade without the explicit “Install older version”
publisher changed see Update flow
disk error the package could not be written

Both are evaluated at install and update time only, against the plugins currently installed (whether they are enabled or not). There is no automatic installation of dependencies.

  • dependencies: for every entry the plugin id must be installed and its installed version must match version. Otherwise the install fails with “missing dependency (<id>)”. The user must first install the dependency (a version in range).
  • conflicts (declared by your plugin): if a plugin with that id is installed and either your entry has no version, or the installed version is in the range, the install fails with “conflicts with an installed plugin (<id>)”.
  • conflicts (declared by others): the check works in both directions — if an installed plugin declares a conflict with your id (without a range, or a range that includes your version), your install is refused too.
  • Removing a plugin that others depend on is not blocked by the host; the dependent plugin simply finds it missing at run time. Design for that (for example, handle the absence of plugin.message replies).

Your own id in either list is a manifest error.

Installing a package with the same id as an installed plugin is an update. Version comparison is semver (see version):

Opened vs installed Result
higher update
equal refused (“this version is already installed”); the dialog offers only Details/Close
lower refused unless the user picks Install older version (explicit downgrade)

1.0.0-rc.1 is lower than 1.0.0; 1.0.1 is higher than 1.0.0; build metadata is not allowed.

If the installed version is signed and the new package has a different publisher (a different key, or no signature), the install is stopped with the question “The publisher of this plugin has changed. Install it anyway?”. Confirming repeats the install with the change accepted. If the installed version was unsigned, there is no stored publisher and any package may update it. So: sign every release with the same key from the first one — users who start with a signed version get this protection for free.

  1. The new package is stored under the plugin’s directory as a new version; the plugin is stopped.
  2. The previous version is kept as the fallback and the update is marked pending; the plugin’s enabled state is kept (an update never enables a disabled plugin).
  3. The plugin restarts at its next trigger. When the new version delivers its first event successfully, the update is committed and the old version is deleted.
  4. If the new version fails before that (it crashes, times out, cannot load), the host rolls back automatically: the new version is deleted, the old one is current again, and the plugin’s log gets rolled back to <version>.
  5. After a commit, repeated failures (3 within 10 minutes) disable the plugin with the reason “stopped after repeated failures”; between failures restarts are delayed 1, 2, 4 … up to 32 seconds.
  • Grants are kept for permissions still in the new manifest. Permissions the new manifest no longer requests are removed from the grants.

  • The dialog for an update shows the whole permission list, with the user’s current choices pre-selected. Anything new is marked New and is pre-selected too; optional ones can be switched off before pressing Update. Required ones cannot be.

  • What counts as new (NewRequests): a permission id that was not in the previous manifest, plus two escalations of an existing one:

    • messages.read:all — messages.read whose scope was not all becomes all;
    • fs.folder:write — fs.folder whose mode was not write becomes write.

    Other changes (a changed reason, different ports, a different required/optional placement) are not flagged.

  • A permission the user did not grant stays denied: calls fail with Denied (Permissions). Make new capabilities optional and degrade gracefully.

Practical rules: do not add required permissions in minor releases unless you must, explain every new permission in its reason, and bump compat.freshgram / testedWith as you test new client versions.

Everything is under the app’s data directory, tdata/plugins/store/:

Path Content
state.json Which plugins are installed, enabled or not, current/previous version, publisher fingerprint, package hashes, grants (protected by a MAC, so edits are detected)
<id>/<version>/package.fgpl The installed package, exactly as received. Its hash is checked every time the plugin is enabled
<id>/data/store.cbor The plugin’s private key–value storage (storage.private), with a backup copy used to recover from damage

The plugin’s storage is shared by all versions: an update keeps data/store.cbor. Quota: runtime.limits.storageMb (default 10 MB). Plugin code only ever sees its own storage through the SDK, never these paths.

The host does not migrate your data. storage.schema (default 1) is a number you raise when the layout of your stored keys changes in an incompatible way; your plugin code must read old data itself (a good habit: store a version key and upgrade on start).

storage.schema matters for settings export/import, available when the manifest has storage.export:

  • In the plugin’s details, Export settings writes a <id>.fgpls file with every storage key that starts with one of export.prefixes and with none of export.exclude, together with the plugin id, its version and storage.schema. At most 4096 keys are exported.
  • Import settings reads such a file (up to 16 MiB) and writes its values into the storage, replacing the same keys. Checks, with the error shown to the user:
Error Cause
invalid not a settings file, bad structure, a key outside export.prefixes/in exclude, a value over 1 MiB
wrong plugin the file was exported by another plugin id
from a newer version of the plugin the file’s storageSchema is greater than the installed manifest’s storage.schema. Files with an equal or lower schema are accepted
over quota the result would exceed storageMb or 4096 keys
unavailable the plugin does not declare storage.export

The plugin is stopped before the import and starts again at its next trigger, so it re-reads the values. Keys that the file does not contain are left untouched. Do not put secrets (tokens) under exported prefixes — or list them in exclude.

Action Effect
Disable (switch in the plugin list) The running plugin is stopped, its timers are cleared, nothing is delivered. Files, grants and storage stay. Enabling again re-checks the stored package against its hash
Automatic disable After repeated failures (see above) or when the stored package is damaged; the reason is shown in the plugin’s details
Remove plugin After the confirmation “Remove {name} together with its data?” the plugin is stopped and its whole directory (<id>/…, including data/) is deleted, together with its settings page and badges. Grants are forgotten

Reinstalling after removing starts with empty storage and a fresh permission dialog.