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.
Clipboard
Section titled “Clipboard”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.
clipboard.write
Section titled “clipboard.write”| 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).
clipboard.read
Section titled “clipboard.read”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"}User-chosen files
Section titled “User-chosen files”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).
files.pick
Section titled “files.pick”| 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}files.save
Section titled “files.save”| 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}The chosen folder (fs.*)
Section titled “The chosen folder (fs.*)”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”.
Path rules
Section titled “Path rules”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.
fs.list
Section titled “fs.list”| 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.
fs.stat
Section titled “fs.stat”Argument path (text, required, not empty). Result: one entry {"name", "dir", "size", "modified"}. not_found if missing.
fs.read
Section titled “fs.read”| 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).
fs.write
Section titled “fs.write”| 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”.
fs.mkdir
Section titled “fs.mkdir”Argument path. Creates one directory (the parent must exist). An existing directory is fine. Result true.
fs.remove
Section titled “fs.remove”Argument path. Deletes a file or an empty directory (a non-empty one is invalid “not empty”). Links are refused (denied). Result true.
fs.rename
Section titled “fs.rename”| 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}Media of messages
Section titled “Media of messages”Permission media.read. Async supported.
media.get
Section titled “media.get”| 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}media.list
Section titled “media.list”| 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”).
Images (image.*)
Section titled “Images (image.*)”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}}