Skip to content

Files, clipboard, media and images

For the call envelope, the order of checks, async rules and the common limits table see System operations. All sizes are in bytes of the raw data.

Permission clipboard. Both calls work only within 10 seconds after a user action (a click on a menu item, command, button, badge or toast action; see System operations); otherwise denied “only right after a user action”. Async is supported.

Argument Type Required Validation
text text yes up to 64 KiB (65536 bytes); else invalid “text”

Result: null. Errors: denied, invalid, failed (or another code from the app) with the message “the clipboard was not changed”, unavailable (the app did not answer in time).

No arguments. Result: text (the current clipboard text, empty string if it has none). Errors: denied, limit “the clipboard text is too large” (over 64 KiB), unavailable.

freshgram.clipboard.write("copied") # raw: {"op": "clipboard.write", "a": {"text": "copied"}}
text = freshgram.clipboard.read() # raw: {"op": "clipboard.read"}

Permission fs.user_selected. Both calls are await-only (aio.files.pick, aio.files.save; a sync call is invalid “files.pick must be awaited”) and require a user action in the last 10 seconds (denied “only right after a user action”). The user answers in a system dialog; the app waits up to 5 minutes (then unavailable).

Argument Type Required Default Validation
title text no the app’s own title up to 64 bytes
filter text no all files up to 256 bytes, a dialog filter such as "Images (*.png *.jpg)"

Result: {"name": <file name only, no directory>, "data": <bytes>}, or null when the user cancels. Files over 100 MiB: limit “the file is too large”. failed “the file was not read” when it cannot be opened. A title or filter that is not text or is too long (over 64 / 256 bytes) is invalid “title or filter”.

f = await aio.files.pick(title="Choose a picture", filter="Images (*.png *.jpg)")
if f:
print(f["name"], len(f["data"]))
# raw: {"op": "files.pick", "a": {"title": "...", "filter": "..."}, "async": true}
Argument Type Required Validation
name text yes suggested file name: 1 to 128 bytes, no / and no \; else invalid “name or data”
data bytes or text yes text is saved as UTF-8; over 100 MiB limit “the file is too large”

Result: true when saved, false when the user cancelled. The dialog starts in the Downloads folder with name suggested; the user may pick another name or place. failed “the file was not saved” if the write fails.

ok = await aio.files.save("report.txt", "hello")
# raw: {"op": "files.save", "a": {"name": "report.txt", "data": "hello"}, "async": true}

Permission fs.folder, with the manifest field mode: "read" (listing and reading) or "write" (also writing). The user picks one folder when granting the permission; the plugin sees only that folder. All fs.* calls are await-only (aio.fs, sync is invalid “fs.list must be awaited” and so on). There is no user-action requirement.

Errors common to all fs.* calls:

Error Trigger
denied “fs.folder” the permission is not granted
unavailable “no folder was chosen” the permission is granted but the user has not chosen a folder
denied “this folder is read only” a write call (fs.write, fs.mkdir, fs.remove, fs.rename) while the mode is read
invalid bad path or argument (below)
not_found the path does not exist
denied (message of the call) the target is not a plain file or directory (for example a link), a link on the path, a permission error, or a directory where a file is needed
limit size limits below, disk full or quota exceeded, listing of more than 100000 entries
failed any other system error

For the failures that come from the file system the message is fixed per operation (the code depends on the cause, see the table above): fs.list “the folder was not listed”; fs.stat “no such file”; fs.read “the file was not read”; fs.write “the file was not written”; fs.mkdir and fs.remove “the change was not made”; fs.rename “the file was not renamed”. A target that already exists is always invalid “already exists”, and a non-empty directory invalid “not empty”.

Paths are text, relative to the chosen folder, with / as separator (on every system).

Rule Detail
Length at most 1024 bytes, no NUL byte
Absolute a leading / is invalid. There is no way to name a path outside the folder
Components empty parts and . are skipped (a//b is a/b); .. is invalid
Depth at most 32 components
Names each at most 255 bytes, valid UTF-8, no control characters (below 0x20, 0x7F), no backslash. On Windows also none of the characters < > : " ? * and the vertical bar, and no trailing dot or space
Links symbolic links are never followed: links on the way or at the end are refused (denied); links in listings are not shown
Empty path allowed only for fs.list (the folder itself)

The folder the user may choose cannot be a filesystem root, the home directory (or a parent of it), or a system location such as /home, /tmp, /var, /opt, /usr, /etc, /mnt, /media, /Library, /Applications.

Argument Type Required Default Validation
path text no "" (the folder) path rules
offset integer no 0 0 or more
limit integer no 200 1 or more; the page is at most 1000 entries. A non-integer offset or limit is invalid “path, offset or limit” (not replaced by the default)

Result: {"entries": [{"name", "dir", "size", "modified"}, ...], "more": <bool>}. Entries are sorted by name; only regular files and directories appear. size is in bytes (0 for directories), modified is a unix time in seconds. more is true when there are entries after this page. A folder with more than 100000 entries: limit.

Argument path (text, required, not empty). Result: one entry {"name", "dir", "size", "modified"}. not_found if missing.

Argument Type Required Default Validation
path text yes file path; a directory is denied
offset integer no 0 0 or more; past the end gives empty bytes
length integer no 100 MiB 0 or more; over 100 MiB is limit “the read is too large”; a bad path, offset or length is invalid “path, offset or length”

Result: bytes (may be shorter than length at the end of the file).

Argument Type Required Default Validation
path text yes the parent directories must already exist
data bytes or text yes text is written as UTF-8; over 100 MiB limit “the file is too large”
mode text no "replace" "create", "replace" or "append"; else invalid “mode”

Modes: create fails with invalid “already exists” when the file exists; replace creates the file or truncates the existing one; append creates the file or adds to its end. New files get owner-only permissions. A file with several hard links is refused (denied). Result: true.

Write budget: at most 512 MiB written per rolling 60 seconds (the size of the data is counted even when the write later fails): quota “too much was written lately”.

Argument path. Creates one directory (the parent must exist). An existing directory is fine. Result true.

Argument path. Deletes a file or an empty directory (a non-empty one is invalid “not empty”). Links are refused (denied). Result true.

Argument Type Required
from text yes
to text yes

Moves or renames a file or directory inside the folder. If to exists: invalid “already exists”. Both paths must follow the path rules. Result true.

await aio.fs.write("notes/today.txt", "hello", mode="append")
page = await aio.fs.list("notes", limit=100)
# raw: {"op": "fs.write", "a": {"path": "notes/today.txt", "data": "hello", "mode": "append"}, "async": true}
# raw: {"op": "fs.rename", "a": {"from": "a.txt", "to": "b.txt"}, "async": true}

Permission media.read. Async supported.

Argument Type Required Default Validation
chat integer yes a chat that is in the plugin’s scope
message integer yes message id
thumbnail bool no false non-bool is invalid
download bool no false non-bool is invalid

Result: {"kind": <text>, "mime": <text>, "name": <text>, "data": <bytes>}.

Field Content
kind "thumbnail" when thumbnail is true, else the media kind: photo, video, round, gif, voice, audio, sticker, file
mime image/jpeg for photos and thumbnails, otherwise the document’s mime type
name the file name of a document; empty for photos and thumbnails
data the file content (up to 100 MiB)

By default only a file already on this device is returned. With download: true a file that is not loaded is downloaded first (up to 5 minutes; use aio.media.get). Photos are returned as JPEG; thumbnails as JPEG; self-destructing media is never returned.

Error Trigger
denied “media.read” permission not granted
invalid “chat, message, thumbnail or download” missing or wrong type
denied “this chat” the chat is the service chat 777000, or not in the chats selected for the plugin
not_found “chat” no such chat, or a secret chat or Saved Messages
denied “messages.read_deleted” the message was deleted and the plugin lacks that permission
not_found “the media is not downloaded” no file on the device (and download false), no media, or self-destructing media
limit the media is over 100 MiB
failed / unavailable the download failed / the app did not answer in time
m = await aio.media.get(chat, message, download=True)
# raw: {"op": "media.get", "a": {"chat": 123, "message": 456, "thumbnail": false, "download": true}, "async": true}
Argument Type Required Default Validation
chat integer yes as above
kind text no "any" one of any, photo, video, photo_video, file, voice, round, audio, gif (up to 16 bytes)
limit integer no 20 clamped to 1 to 100
before integer no 0 message id to start before; negative is invalid

Result: array of message maps, newest first, in the same shape as messages.history items (with a media field). Deleted messages are included only with messages.read_deleted. Errors as in media.get (invalid “chat, kind or before”, denied “this chat”, not_found “chat”).

Permission image.process. The app decodes and encodes the pictures. Async supported. A shared rate limit of one call per 100 ms covers all four operations (limit “pictures are processed too often”); it is counted before the checks, so even an invalid call uses the slot.

Common input and validation:

Argument Type Default Validation
data bytes required not empty; over 16 MiB limit “the picture is too large”; must start like PNG, JPEG or WebP, else invalid “a picture is a PNG, a JPEG or a WebP”
format text keep the input format "png", "jpeg" or "webp", else invalid
quality integer 90 1 to 100, else invalid “quality is 1 to 100”
width, height integer 0 0 to 8192
x, y integer 0 0 to 8192
fit text "contain" "contain", "cover" or "exact"

A non-integer quality is invalid (“quality is 1 to 100”); a non-integer or out-of-range width, height, x or y is invalid (“width, height, x or y”). Animated pictures (more than one frame) are invalid; a picture over 24 megapixels is limit; an unreadable picture is failed; a result over 32 MiB is limit “the result is too large”; if the app cannot write the chosen format (for example WebP support missing) the answer is unavailable.

Result: {"format": "png" | "jpeg" | "webp", "width": <int>, "height": <int>, "data": <bytes>}; image.info has no data.

Operation Required arguments Behavior
image.info data the format and size of the input
image.resize data, width and/or height contain (default): fit inside the box, with one side 0 the aspect ratio is kept; cover and exact need both sizes (invalid “cover and exact need both width and height”); cover fills the box and cuts the overflow around the center, exact stretches. A result side over 8192 is limit. With neither side invalid “resize needs width or height”
image.convert data, format re-encode; invalid “convert needs a format” if format is missing. Transparent areas become white when writing JPEG
image.crop data, width, height (x, y default 0) keeps the rectangle with the corner x, y; the rectangle is cut to the picture, an empty result is invalid; invalid “crop needs width and height”
info = freshgram.image.info(data)
small = freshgram.image.resize(data, width=256, format="webp", quality=80)
# raw: {"op": "image.resize", "a": {"data": <bytes>, "width": 256, "fit": "contain", "format": "webp", "quality": 80}}