meta-sovereign 0.49.13 → 0.49.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/src/server/routes-telegram-channel-manage.js +79 -0
- package/js/src/server/routes-telegram-emoji-groups.js +20 -0
- package/js/src/server/routes-telegram-storage.js +73 -0
- package/js/src/server/routes-telegram-translate-chat.js +31 -0
- package/js/src/server/routes-telegram-yextras.js +17 -0
- package/js/src/server/routes-telegram.js +2 -0
- package/js/src/sources/telegram-actions.js +17 -2
- package/js/src/sources/telegram-channel-manage.js +263 -0
- package/js/src/sources/telegram-chat-info.js +16 -0
- package/js/src/sources/telegram-chat-removal.js +21 -2
- package/js/src/sources/telegram-emoji-groups.js +95 -0
- package/js/src/sources/telegram-format.js +39 -4
- package/js/src/sources/telegram-notify-extras.js +144 -0
- package/js/src/sources/telegram-stickers.js +6 -1
- package/js/src/sources/telegram-storage.js +380 -0
- package/js/src/sources/telegram-translate-chat.js +57 -0
- package/js/src/sources/telegram-translate.js +5 -5
- package/js/src/web/app.css +157 -2
- package/js/src/web/app.js +2 -0
- package/js/src/web/auto-download.js +37 -0
- package/js/src/web/chat-actions.js +1 -0
- package/js/src/web/chat-channel-limit.js +142 -0
- package/js/src/web/chat-edit.js +3 -0
- package/js/src/web/chat-emoji-custom.js +338 -0
- package/js/src/web/chat-emoji-status.js +21 -9
- package/js/src/web/chat-emoji-tools.js +17 -1
- package/js/src/web/chat-emoji.js +214 -72
- package/js/src/web/chat-group-stickers.js +3 -10
- package/js/src/web/chat-invite.js +10 -0
- package/js/src/web/chat-manage.js +303 -0
- package/js/src/web/chat-message.js +16 -8
- package/js/src/web/chat-profile-extras.js +3 -3
- package/js/src/web/chat-public-link.js +314 -0
- package/js/src/web/chat-selection.js +2 -1
- package/js/src/web/chat-similar.js +14 -1
- package/js/src/web/chat-translate-bar.js +293 -0
- package/js/src/web/chat-translate.js +93 -27
- package/js/src/web/chat-translation-switch.js +20 -22
- package/js/src/web/chat-view.js +3 -0
- package/js/src/web/dom-telegram-calls.js +27 -0
- package/js/src/web/locales/en-chat-account.js +105 -0
- package/js/src/web/locales/hi-chat-account.js +106 -0
- package/js/src/web/locales/ru-chat-account.js +106 -0
- package/js/src/web/locales/zh-chat-account.js +99 -0
- package/js/src/web/settings-confirm.js +62 -0
- package/js/src/web/settings-fields.js +47 -0
- package/js/src/web/settings-media.js +21 -1
- package/js/src/web/settings-notify-extras.js +160 -0
- package/js/src/web/settings-notify.js +16 -4
- package/js/src/web/settings-storage.js +235 -0
- package/js/src/web/settings-view.js +5 -1
- package/js/src/web/translate-detect.js +108 -0
- package/js/src/web/translate-store.js +140 -0
- package/js/src/web/tutorial-clear.js +197 -0
- package/js/src/web/tutorial.js +48 -4
- package/js/src/web/use-redraw.js +14 -0
- package/package.json +10 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Managing a channel or group (R-Y216): the routes of the POSTS table in
|
|
2
|
+
// routes-telegram.js, each validated strictly before the account is asked.
|
|
3
|
+
// The source validates again; this keeps junk off the account. Deleting a
|
|
4
|
+
// chat for everyone is `remove-chat` with the action `destroy`.
|
|
5
|
+
//
|
|
6
|
+
// Its routes:
|
|
7
|
+
// POST /api/telegram/channel-username-check {chat, username} -> { ok,
|
|
8
|
+
// status } whether the public link is free, or a
|
|
9
|
+
// code: invalid, taken, too-many-public (R-Y216)
|
|
10
|
+
// POST /api/telegram/channel-username-save {chat, username} -> { ok } the
|
|
11
|
+
// public link set; '' makes the chat private
|
|
12
|
+
// POST /api/telegram/public-channels {} -> { ok, channels } the user's
|
|
13
|
+
// public channels, for "Revoke link"
|
|
14
|
+
// POST /api/telegram/public-link-revoke {chat} -> { ok } one of them
|
|
15
|
+
// made private
|
|
16
|
+
// POST /api/telegram/usernames-deactivate {chat} -> { ok } every
|
|
17
|
+
// collectible username of the chat deactivated
|
|
18
|
+
// POST /api/telegram/channel-color-save {chat, forProfile, color?, emoji?}
|
|
19
|
+
// -> { ok } the name or profile colour (boosts needed)
|
|
20
|
+
// POST /api/telegram/channel-emoji-status {chat, documentId, until?} ->
|
|
21
|
+
// { ok } the channel's emoji status (null removes it)
|
|
22
|
+
// POST /api/telegram/inactive-channels {} -> { ok, channels } channels the
|
|
23
|
+
// user could leave when there are too many
|
|
24
|
+
// POST /api/telegram/leave-channels {chats} -> { ok, left } the chosen
|
|
25
|
+
// inactive channels left
|
|
26
|
+
|
|
27
|
+
import { route } from './routes-telegram-admin-extras.js';
|
|
28
|
+
|
|
29
|
+
const CHANNEL = /^-100\d{1,20}$/;
|
|
30
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_]{3,30}[A-Za-z0-9]$/;
|
|
31
|
+
const DIGITS = /^\d{1,20}$/;
|
|
32
|
+
const MAX_LEAVE = 100;
|
|
33
|
+
|
|
34
|
+
const channelOf = (b) => CHANNEL.test(String(b.chat ?? ''));
|
|
35
|
+
const nameOf = (b) => typeof b.username === 'string' && NAME.test(b.username);
|
|
36
|
+
const WHAT = 'chat required';
|
|
37
|
+
|
|
38
|
+
export const CHANNEL_MANAGE_POSTS = {
|
|
39
|
+
'channel-username-check': route(
|
|
40
|
+
'channelUsernameCheck',
|
|
41
|
+
'chat and username required',
|
|
42
|
+
(b) => channelOf(b) && nameOf(b)
|
|
43
|
+
),
|
|
44
|
+
'channel-username-save': route(
|
|
45
|
+
'channelUsernameSave',
|
|
46
|
+
'chat and username required',
|
|
47
|
+
(b) => channelOf(b) && (b.username === '' || nameOf(b))
|
|
48
|
+
),
|
|
49
|
+
'public-channels': route('publicChannels', WHAT, () => true),
|
|
50
|
+
'public-link-revoke': route('publicLinkRevoke', WHAT, channelOf),
|
|
51
|
+
'usernames-deactivate': route('usernamesDeactivate', WHAT, channelOf),
|
|
52
|
+
'channel-color-save': route(
|
|
53
|
+
'channelColorSave',
|
|
54
|
+
'chat and colour required',
|
|
55
|
+
(b) =>
|
|
56
|
+
channelOf(b) &&
|
|
57
|
+
typeof b.forProfile === 'boolean' &&
|
|
58
|
+
(b.color === undefined || Number.isInteger(b.color)) &&
|
|
59
|
+
(b.emoji === undefined || DIGITS.test(String(b.emoji)))
|
|
60
|
+
),
|
|
61
|
+
'channel-emoji-status': route(
|
|
62
|
+
'channelEmojiStatusSet',
|
|
63
|
+
'chat and status required',
|
|
64
|
+
(b) =>
|
|
65
|
+
channelOf(b) &&
|
|
66
|
+
(b.documentId === null || DIGITS.test(String(b.documentId))) &&
|
|
67
|
+
(b.until === undefined || Number.isInteger(b.until))
|
|
68
|
+
),
|
|
69
|
+
'inactive-channels': route('inactiveChannels', WHAT, () => true),
|
|
70
|
+
'leave-channels': route(
|
|
71
|
+
'leaveChannels',
|
|
72
|
+
'chats required',
|
|
73
|
+
(b) =>
|
|
74
|
+
Array.isArray(b.chats) &&
|
|
75
|
+
b.chats.length > 0 &&
|
|
76
|
+
b.chats.length <= MAX_LEAVE &&
|
|
77
|
+
b.chats.every((x) => CHANNEL.test(String(x)))
|
|
78
|
+
),
|
|
79
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The emoji panel's Premium flag and category groups (R-Y214): the route
|
|
2
|
+
// of the POSTS table in routes-telegram.js, validated before the account
|
|
3
|
+
// is asked.
|
|
4
|
+
//
|
|
5
|
+
// Its routes:
|
|
6
|
+
// POST /api/telegram/emoji-panel-info {status?} -> { ok, premium, groups:
|
|
7
|
+
// [{ title, iconEmojiId, emoticons }] } whether the
|
|
8
|
+
// account has Premium and Telegram's emoji groups
|
|
9
|
+
// (status: true, the emoji status picker's)
|
|
10
|
+
|
|
11
|
+
import { route } from './routes-telegram-admin-extras.js';
|
|
12
|
+
|
|
13
|
+
export const EMOJI_GROUPS_POSTS = {
|
|
14
|
+
'emoji-panel-info': route(
|
|
15
|
+
'emojiPanelInfo',
|
|
16
|
+
'status must be true or false',
|
|
17
|
+
(b) => b.status === undefined || typeof b.status === 'boolean',
|
|
18
|
+
(b) => ({ status: b.status === true })
|
|
19
|
+
),
|
|
20
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Local storage management, reaction notifications, resetting the
|
|
2
|
+
// notification settings and Telegram's automatic-download preset (R-Y215):
|
|
3
|
+
// the routes of the POSTS table in routes-telegram.js, each validated
|
|
4
|
+
// strictly before the account is asked.
|
|
5
|
+
//
|
|
6
|
+
// Its routes:
|
|
7
|
+
// POST /api/telegram/storage-usage {} -> { ok, total, kinds, chats,
|
|
8
|
+
// limits } the media cache by kind (photos, videos,
|
|
9
|
+
// voice, files, stickers, other) and by chat
|
|
10
|
+
// POST /api/telegram/storage-clear {kind} | {all: true} -> { ok,
|
|
11
|
+
// removed: { bytes, files } } only files inside the
|
|
12
|
+
// media cache are removed
|
|
13
|
+
// POST /api/telegram/storage-limits {} -> { ok, maxGb, olderDays }
|
|
14
|
+
// POST /api/telegram/storage-limits-set {maxGb, olderDays} -> { ok, ...,
|
|
15
|
+
// removed } maxGb 0, 1, 5 or 20; olderDays 0, 7, 30
|
|
16
|
+
// or 365 (0 is none); applied at once
|
|
17
|
+
// POST /api/telegram/reaction-notify {} -> { ok, messages, stories,
|
|
18
|
+
// previews, sound } who is told of reactions
|
|
19
|
+
// ('nobody', 'contacts', 'all')
|
|
20
|
+
// POST /api/telegram/reaction-notify-set {messages?, stories?, previews?,
|
|
21
|
+
// sound?} -> the same row
|
|
22
|
+
// POST /api/telegram/notify-reset {} -> { ok } every notification
|
|
23
|
+
// setting back to Telegram's default
|
|
24
|
+
// POST /api/telegram/auto-download-preset {} -> { ok, medium: {
|
|
25
|
+
// disabled, photoMax, videoMax, fileMax } } bytes
|
|
26
|
+
|
|
27
|
+
import { validClear, validLimits } from '../sources/telegram-storage.js';
|
|
28
|
+
import { validReactionChange } from '../sources/telegram-notify-extras.js';
|
|
29
|
+
import { route } from './routes-telegram-admin-extras.js';
|
|
30
|
+
|
|
31
|
+
const anyone = () => true;
|
|
32
|
+
const nothing = () => ({});
|
|
33
|
+
|
|
34
|
+
export const STORAGE_POSTS = {
|
|
35
|
+
'storage-usage': route('storageUsage', 'not supported', anyone, nothing),
|
|
36
|
+
'storage-clear': route(
|
|
37
|
+
'storageClear',
|
|
38
|
+
'a kind or all required',
|
|
39
|
+
validClear,
|
|
40
|
+
(b) => (b.all === true ? { all: true } : { kind: b.kind })
|
|
41
|
+
),
|
|
42
|
+
'storage-limits': route('storageLimits', 'not supported', anyone, nothing),
|
|
43
|
+
'storage-limits-set': route(
|
|
44
|
+
'storageLimitsSet',
|
|
45
|
+
'maxGb and olderDays required',
|
|
46
|
+
validLimits,
|
|
47
|
+
(b) => ({ maxGb: b.maxGb, olderDays: b.olderDays })
|
|
48
|
+
),
|
|
49
|
+
'reaction-notify': route(
|
|
50
|
+
'reactionNotifyGet',
|
|
51
|
+
'not supported',
|
|
52
|
+
anyone,
|
|
53
|
+
nothing
|
|
54
|
+
),
|
|
55
|
+
'reaction-notify-set': route(
|
|
56
|
+
'reactionNotifySet',
|
|
57
|
+
'a change required',
|
|
58
|
+
validReactionChange,
|
|
59
|
+
(b) => ({
|
|
60
|
+
messages: b.messages,
|
|
61
|
+
stories: b.stories,
|
|
62
|
+
previews: b.previews,
|
|
63
|
+
sound: b.sound,
|
|
64
|
+
})
|
|
65
|
+
),
|
|
66
|
+
'notify-reset': route('notifyResetAll', 'not supported', anyone, nothing),
|
|
67
|
+
'auto-download-preset': route(
|
|
68
|
+
'autoDownloadPreset',
|
|
69
|
+
'not supported',
|
|
70
|
+
anyone,
|
|
71
|
+
nothing
|
|
72
|
+
),
|
|
73
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The translate bar and the summary (R-Y213): routes of the POSTS table in
|
|
2
|
+
// routes-telegram.js, validated strictly before the account is asked.
|
|
3
|
+
//
|
|
4
|
+
// Its routes:
|
|
5
|
+
// POST /api/telegram/translate-account {} -> { ok, premium } whether the
|
|
6
|
+
// account has Premium (the bar is offered only then)
|
|
7
|
+
// POST /api/telegram/summarize {chat, id, to?} -> { ok, text, entities }
|
|
8
|
+
// a long message summarized (messages.summarizeText);
|
|
9
|
+
// refusals come as { ok: false, code }
|
|
10
|
+
|
|
11
|
+
import { route } from './routes-telegram-admin-extras.js';
|
|
12
|
+
|
|
13
|
+
const LANG = /^[a-z]{2,3}(-[A-Za-z]{2,4})?$/;
|
|
14
|
+
|
|
15
|
+
export const TRANSLATE_CHAT_POSTS = {
|
|
16
|
+
'translate-account': route(
|
|
17
|
+
'translateAccount',
|
|
18
|
+
'nothing required',
|
|
19
|
+
() => true,
|
|
20
|
+
() => ({})
|
|
21
|
+
),
|
|
22
|
+
summarize: route(
|
|
23
|
+
'summarizeMessage',
|
|
24
|
+
'chat and id required',
|
|
25
|
+
(b) =>
|
|
26
|
+
/^-?\d{1,20}$/.test(String(b.chat ?? '')) &&
|
|
27
|
+
Number.isInteger(b.id) &&
|
|
28
|
+
b.id > 0 &&
|
|
29
|
+
(b.to === undefined || (typeof b.to === 'string' && LANG.test(b.to)))
|
|
30
|
+
),
|
|
31
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The route tables of the R-Y213..R-Y216 features in one spread, so that
|
|
2
|
+
// routes-telegram.js stays under its line limit. Each table documents its
|
|
3
|
+
// own routes in its file's header:
|
|
4
|
+
// routes-telegram-translate-chat.js (R-Y213), -emoji-groups.js (R-Y214),
|
|
5
|
+
// -storage.js (R-Y215), -channel-manage.js (R-Y216).
|
|
6
|
+
|
|
7
|
+
import { TRANSLATE_CHAT_POSTS } from './routes-telegram-translate-chat.js';
|
|
8
|
+
import { EMOJI_GROUPS_POSTS } from './routes-telegram-emoji-groups.js';
|
|
9
|
+
import { STORAGE_POSTS } from './routes-telegram-storage.js';
|
|
10
|
+
import { CHANNEL_MANAGE_POSTS } from './routes-telegram-channel-manage.js';
|
|
11
|
+
|
|
12
|
+
export const YEXTRAS_POSTS = {
|
|
13
|
+
...TRANSLATE_CHAT_POSTS,
|
|
14
|
+
...EMOJI_GROUPS_POSTS,
|
|
15
|
+
...STORAGE_POSTS,
|
|
16
|
+
...CHANNEL_MANAGE_POSTS,
|
|
17
|
+
};
|
|
@@ -206,6 +206,7 @@ import { WEB_APPS_POSTS } from './routes-telegram-web-apps.js';
|
|
|
206
206
|
import { BOT_LOGIN_POSTS } from './routes-telegram-bot-login.js';
|
|
207
207
|
import { ADMIN_POSTS } from './routes-telegram-admin.js';
|
|
208
208
|
import { GROUP_EXTRAS_POSTS } from './routes-telegram-group-extras.js';
|
|
209
|
+
import { YEXTRAS_POSTS } from './routes-telegram-yextras.js';
|
|
209
210
|
import {
|
|
210
211
|
INSTANT_VIEW_GETS,
|
|
211
212
|
INSTANT_VIEW_POSTS,
|
|
@@ -1020,6 +1021,7 @@ const POSTS = {
|
|
|
1020
1021
|
: [400, { error: 'chat and a change required', code: 'bad-request' }],
|
|
1021
1022
|
...ADMIN_POSTS,
|
|
1022
1023
|
...EPHEMERAL_POSTS,
|
|
1024
|
+
...YEXTRAS_POSTS,
|
|
1023
1025
|
...GROUP_EXTRAS_POSTS,
|
|
1024
1026
|
...INSTANT_VIEW_POSTS,
|
|
1025
1027
|
...STICKER_EXTRAS_POSTS,
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* ./telegram-scheduled.js (R-Y102), chat folders ./telegram-folders.js
|
|
27
27
|
* (R-Y103); translating messages is ./telegram-translate.js (R-Y132);
|
|
28
28
|
* message effects are ./telegram-effects.js (R-Y149); emoji statuses
|
|
29
|
-
* ./telegram-emoji-status.js (R-Y162)
|
|
29
|
+
* ./telegram-emoji-status.js (R-Y162); the translate bar and summaries
|
|
30
|
+
* ./telegram-translate-chat.js (R-Y213).
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import { updatesOfResult, entitiesOf } from './telegram-catchup.js';
|
|
@@ -45,6 +46,7 @@ import { createChatSearch } from './telegram-search.js';
|
|
|
45
46
|
import { createGlobalSearch } from './telegram-global.js';
|
|
46
47
|
import { createFileSender } from './telegram-upload.js';
|
|
47
48
|
import { createChatRemoval } from './telegram-chat-removal.js';
|
|
49
|
+
import { createChannelManage } from './telegram-channel-manage.js';
|
|
48
50
|
import { createScheduled } from './telegram-scheduled.js';
|
|
49
51
|
import { createFolders } from './telegram-folders.js';
|
|
50
52
|
import { createFolderEdit } from './telegram-folder-edit.js';
|
|
@@ -78,6 +80,7 @@ import { createReadersActions } from './telegram-readers.js';
|
|
|
78
80
|
import { createMapActions } from './telegram-map.js';
|
|
79
81
|
import { createChatEdit } from './telegram-chat-edit.js';
|
|
80
82
|
import { createTranslate } from './telegram-translate.js';
|
|
83
|
+
import { createTranslateChat } from './telegram-translate-chat.js';
|
|
81
84
|
import { createTranscribe } from './telegram-transcribe.js';
|
|
82
85
|
import { createEphemeral } from './telegram-ephemeral.js';
|
|
83
86
|
import { createEffects } from './telegram-effects.js';
|
|
@@ -97,6 +100,9 @@ import { createTopicAdmin } from './telegram-topic-admin.js';
|
|
|
97
100
|
import { createChatReactions } from './telegram-chat-reactions.js';
|
|
98
101
|
import { createChatSettings } from './telegram-chat-settings.js';
|
|
99
102
|
import { createGroupExtras } from './telegram-group-extras.js';
|
|
103
|
+
import { createEmojiGroups } from './telegram-emoji-groups.js';
|
|
104
|
+
import { createStorage } from './telegram-storage.js';
|
|
105
|
+
import { createNotifyExtras } from './telegram-notify-extras.js';
|
|
100
106
|
import { createNotifySettings } from './telegram-notify-settings.js';
|
|
101
107
|
import { createRingtones } from './telegram-ringtones.js';
|
|
102
108
|
import { createPinnedOrder } from './telegram-pinned-order.js';
|
|
@@ -499,7 +505,11 @@ export const createActions = (env, tools, { mediaDir = null } = {}) => {
|
|
|
499
505
|
// exporting the account's data (R-Y212); a group's sticker set and emoji
|
|
500
506
|
// pack, pinned topics reordered, new chats in a shared folder and the
|
|
501
507
|
// new-contacts notification (R-Y211); ephemeral messages (R-Y223); deleted
|
|
502
|
-
// messages marked with the one tombstone shape (R-Z7)
|
|
508
|
+
// messages marked with the one tombstone shape (R-Z7); custom emoji in the
|
|
509
|
+
// composer and the emoji panel's category groups (R-Y214); the media
|
|
510
|
+
// cache's management, reaction notifications and resetting notifications
|
|
511
|
+
// (R-Y215); a channel's public link, look and deletion, and inactive
|
|
512
|
+
// channels to leave (R-Y216).
|
|
503
513
|
const chatActions = (env, context) => {
|
|
504
514
|
const { held, ...files } = createFileSender(env, context);
|
|
505
515
|
const { uploads, drop } = held;
|
|
@@ -522,6 +532,7 @@ const chatActions = (env, context) => {
|
|
|
522
532
|
...createGlobalSearch(env, context),
|
|
523
533
|
...files,
|
|
524
534
|
...createChatRemoval(env, context),
|
|
535
|
+
...createChannelManage(env, context),
|
|
525
536
|
...createScheduled(env, context),
|
|
526
537
|
...createFolders(env, context),
|
|
527
538
|
...createFolderEdit(env, context),
|
|
@@ -560,6 +571,7 @@ const chatActions = (env, context) => {
|
|
|
560
571
|
...createApps(env, { ...context, offer: inlineOffer }),
|
|
561
572
|
...createMapActions(env, context),
|
|
562
573
|
...createTranslate(env, context),
|
|
574
|
+
...createTranslateChat(env, context),
|
|
563
575
|
...createTranscribe(env, context),
|
|
564
576
|
...createEphemeral(env, context),
|
|
565
577
|
...createStories(env, context),
|
|
@@ -623,6 +635,9 @@ const chatActions = (env, context) => {
|
|
|
623
635
|
...createCalendar(env, context),
|
|
624
636
|
...createTopPeers(env, context),
|
|
625
637
|
...createGroupExtras(env, context),
|
|
638
|
+
...createEmojiGroups(env, context),
|
|
639
|
+
...createStorage(env, context),
|
|
640
|
+
...createNotifyExtras(env, context),
|
|
626
641
|
};
|
|
627
642
|
};
|
|
628
643
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managing a channel or group, as Telegram Desktop's Edit has it (R-Y216;
|
|
3
|
+
* learned, never copied):
|
|
4
|
+
* - `channelUsernameCheck({ chat, username })` — channels.checkUsername
|
|
5
|
+
* → `{ ok, status: 'available' }`; USERNAME_INVALID, USERNAME_OCCUPIED
|
|
6
|
+
* and CHANNELS_ADMIN_PUBLIC_TOO_MUCH come back as codes;
|
|
7
|
+
* - `channelUsernameSave({ chat, username })` — channels.updateUsername
|
|
8
|
+
* (an empty name makes the chat private);
|
|
9
|
+
* - `publicChannels()` — channels.getAdminedPublicChannels → `{ ok,
|
|
10
|
+
* channels: [{ chat, title, username }] }`, for the limit's "Revoke link";
|
|
11
|
+
* - `publicLinkRevoke({ chat })` — one of those channels made private;
|
|
12
|
+
* - `usernamesDeactivate({ chat })` — channels.deactivateAllUsernames;
|
|
13
|
+
* - `channelColorSave({ chat, forProfile, color?, emoji? })` —
|
|
14
|
+
* channels.updateColor (name colour or profile colour, with a background
|
|
15
|
+
* emoji); BOOSTS_REQUIRED comes back as `boosts`;
|
|
16
|
+
* - `channelEmojiStatusSet({ chat, documentId|null, until? })` —
|
|
17
|
+
* channels.updateEmojiStatus;
|
|
18
|
+
* - `inactiveChannels()` — channels.getInactiveChannels → `{ ok, channels:
|
|
19
|
+
* [{ chat, title, date, broadcast }] }`, for the "too many channels" case;
|
|
20
|
+
* - `leaveChannels({ chats })` — channels.leaveChannel on the chosen ones,
|
|
21
|
+
* only those Telegram lists as inactive.
|
|
22
|
+
* Deleting a chat is `removeChat` with the `destroy` action.
|
|
23
|
+
* Telegram's errors become codes the page shows as text.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { connected, runIn } from './telegram-stickers.js';
|
|
27
|
+
import { quietFailureOf } from './telegram-chat-settings.js';
|
|
28
|
+
|
|
29
|
+
const CODES = {
|
|
30
|
+
USERNAME_INVALID: 'invalid',
|
|
31
|
+
USERNAME_PURCHASE_AVAILABLE: 'taken',
|
|
32
|
+
USERNAME_OCCUPIED: 'taken',
|
|
33
|
+
USERNAME_NOT_MODIFIED: 'not-modified',
|
|
34
|
+
CHANNELS_ADMIN_PUBLIC_TOO_MUCH: 'too-many-public',
|
|
35
|
+
CHAT_ADMIN_REQUIRED: 'not-admin',
|
|
36
|
+
CHAT_NOT_MODIFIED: 'not-modified',
|
|
37
|
+
CHANNEL_PRIVATE: 'no-access',
|
|
38
|
+
CHANNEL_INVALID: 'no-access',
|
|
39
|
+
BOOSTS_REQUIRED: 'boosts',
|
|
40
|
+
COLOR_INVALID: 'invalid',
|
|
41
|
+
DOCUMENT_INVALID: 'invalid',
|
|
42
|
+
EMOJI_STATUS_INVALID: 'invalid',
|
|
43
|
+
FLOOD_WAIT: 'flood',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const failure = quietFailureOf(CODES);
|
|
47
|
+
const bad = (code = 'bad-request') => ({ ok: false, error: code, code });
|
|
48
|
+
|
|
49
|
+
const CHANNEL = /^-100\d{1,20}$/;
|
|
50
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_]{3,30}[A-Za-z0-9]$/;
|
|
51
|
+
const EMOJI = /^\d{1,20}$/;
|
|
52
|
+
const MAX_COLOR = 20;
|
|
53
|
+
const MAX_LEAVE = 100;
|
|
54
|
+
const MAX_UNTIL_SECONDS = 366 * 24 * 3600;
|
|
55
|
+
|
|
56
|
+
const text = (value) => String(value?.value ?? value);
|
|
57
|
+
const idOf = (raw) => `-100${text(raw.id)}`;
|
|
58
|
+
|
|
59
|
+
const nameOk = (username) =>
|
|
60
|
+
typeof username === 'string' && (username === '' || NAME.test(username));
|
|
61
|
+
|
|
62
|
+
const colorOk = ({ forProfile, color, emoji }) =>
|
|
63
|
+
typeof forProfile === 'boolean' &&
|
|
64
|
+
(color === undefined ||
|
|
65
|
+
(Number.isInteger(color) && color >= 0 && color <= MAX_COLOR)) &&
|
|
66
|
+
(emoji === undefined || EMOJI.test(String(emoji)));
|
|
67
|
+
|
|
68
|
+
const statusOk = (r, seconds) =>
|
|
69
|
+
(r.documentId === null || EMOJI.test(String(r.documentId))) &&
|
|
70
|
+
(r.until === undefined ||
|
|
71
|
+
(Number.isInteger(r.until) &&
|
|
72
|
+
r.until > seconds &&
|
|
73
|
+
r.until <= seconds + MAX_UNTIL_SECONDS));
|
|
74
|
+
|
|
75
|
+
const publicOf = (raw) =>
|
|
76
|
+
raw?.className === 'Channel'
|
|
77
|
+
? { chat: idOf(raw), title: raw.title ?? '', username: raw.username ?? '' }
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
const inactiveOf = (answer) =>
|
|
81
|
+
(answer?.chats ?? [])
|
|
82
|
+
.map((raw, i) => ({ raw, date: Number(answer.dates?.[i]) || 0 }))
|
|
83
|
+
.filter(({ raw }) => raw?.className === 'Channel');
|
|
84
|
+
|
|
85
|
+
// The shared helpers of the actions below.
|
|
86
|
+
const kitOf = (env, userClient) => {
|
|
87
|
+
const ask = (c, key, request) =>
|
|
88
|
+
runIn(env, `channel-manage:${key}`, () => c.client.invoke(request));
|
|
89
|
+
const withChannel = (c, chat, use) =>
|
|
90
|
+
runIn(env, `channel-manage:${chat}`, () =>
|
|
91
|
+
env.connection.withPeer(c.client, chat, use)
|
|
92
|
+
);
|
|
93
|
+
// An action that runs `use(request)(c)` for a request that passed `valid`.
|
|
94
|
+
const guarded = (valid, use) => async (request) =>
|
|
95
|
+
valid(request ?? {})
|
|
96
|
+
? connected(env, userClient, failure, use(request ?? {}))
|
|
97
|
+
: bad();
|
|
98
|
+
// An action of one request on the chat's channel.
|
|
99
|
+
const onChannel = (valid, make) =>
|
|
100
|
+
guarded(
|
|
101
|
+
(r) => CHANNEL.test(String(r.chat ?? '')) && valid(r),
|
|
102
|
+
(r) => async (c) => {
|
|
103
|
+
await withChannel(c, String(r.chat), (peer) =>
|
|
104
|
+
c.client.invoke(make(c, peer, r))
|
|
105
|
+
);
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
const plain = (use) => async () =>
|
|
110
|
+
connected(env, userClient, failure, async (c) => ({
|
|
111
|
+
ok: true,
|
|
112
|
+
...(await use(c)),
|
|
113
|
+
}));
|
|
114
|
+
const admined = async (c) =>
|
|
115
|
+
(
|
|
116
|
+
(await ask(c, 'admined', new c.Api.channels.GetAdminedPublicChannels({})))
|
|
117
|
+
?.chats ?? []
|
|
118
|
+
).filter((x) => x?.className === 'Channel');
|
|
119
|
+
// The inactive channels Telegram lists, with their dates.
|
|
120
|
+
const inactive = async (c) =>
|
|
121
|
+
inactiveOf(
|
|
122
|
+
await ask(c, 'inactive', new c.Api.channels.GetInactiveChannels())
|
|
123
|
+
);
|
|
124
|
+
return { ask, withChannel, guarded, onChannel, plain, admined, inactive };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const createPublicLink = ({
|
|
128
|
+
ask,
|
|
129
|
+
withChannel,
|
|
130
|
+
guarded,
|
|
131
|
+
onChannel,
|
|
132
|
+
admined,
|
|
133
|
+
plain,
|
|
134
|
+
}) => ({
|
|
135
|
+
channelUsernameCheck: guarded(
|
|
136
|
+
(r) => CHANNEL.test(String(r.chat ?? '')) && NAME.test(r.username ?? ''),
|
|
137
|
+
(r) => async (c) => {
|
|
138
|
+
const free = await withChannel(c, String(r.chat), (peer) =>
|
|
139
|
+
c.client.invoke(
|
|
140
|
+
new c.Api.channels.CheckUsername({
|
|
141
|
+
channel: peer,
|
|
142
|
+
username: r.username,
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
return free === false ? bad('taken') : { ok: true, status: 'available' };
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
channelUsernameSave: onChannel(
|
|
150
|
+
(r) => nameOk(r.username),
|
|
151
|
+
(c, peer, r) =>
|
|
152
|
+
new c.Api.channels.UpdateUsername({
|
|
153
|
+
channel: peer,
|
|
154
|
+
username: r.username,
|
|
155
|
+
})
|
|
156
|
+
),
|
|
157
|
+
publicChannels: plain(async (c) => ({
|
|
158
|
+
channels: (await admined(c)).map(publicOf).filter(Boolean),
|
|
159
|
+
})),
|
|
160
|
+
publicLinkRevoke: guarded(
|
|
161
|
+
(r) => CHANNEL.test(String(r.chat ?? '')),
|
|
162
|
+
(r) => async (c) => {
|
|
163
|
+
const found = (await admined(c)).find((x) => idOf(x) === r.chat);
|
|
164
|
+
if (!found) {
|
|
165
|
+
return bad('no-access');
|
|
166
|
+
}
|
|
167
|
+
await ask(
|
|
168
|
+
c,
|
|
169
|
+
'revoke',
|
|
170
|
+
new c.Api.channels.UpdateUsername({
|
|
171
|
+
channel: new c.Api.InputChannel({
|
|
172
|
+
channelId: found.id,
|
|
173
|
+
accessHash: found.accessHash,
|
|
174
|
+
}),
|
|
175
|
+
username: '',
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
),
|
|
181
|
+
usernamesDeactivate: onChannel(
|
|
182
|
+
() => true,
|
|
183
|
+
(c, peer) => new c.Api.channels.DeactivateAllUsernames({ channel: peer })
|
|
184
|
+
),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const createLook = (env, { onChannel }) => ({
|
|
188
|
+
channelColorSave: onChannel(colorOk, (c, peer, r) => {
|
|
189
|
+
const { forProfile, color, emoji } = r;
|
|
190
|
+
return new c.Api.channels.UpdateColor({
|
|
191
|
+
channel: peer,
|
|
192
|
+
...(forProfile ? { forProfile: true } : {}),
|
|
193
|
+
...(color === undefined ? {} : { color }),
|
|
194
|
+
...(emoji === undefined
|
|
195
|
+
? {}
|
|
196
|
+
: { backgroundEmojiId: c.helpers.returnBigInt(String(emoji)) }),
|
|
197
|
+
});
|
|
198
|
+
}),
|
|
199
|
+
channelEmojiStatusSet: onChannel(
|
|
200
|
+
(r) => statusOk(r, (env.now ?? Date.now)() / 1000),
|
|
201
|
+
(c, peer, r) =>
|
|
202
|
+
new c.Api.channels.UpdateEmojiStatus({
|
|
203
|
+
channel: peer,
|
|
204
|
+
emojiStatus:
|
|
205
|
+
r.documentId === null
|
|
206
|
+
? new c.Api.EmojiStatusEmpty()
|
|
207
|
+
: new c.Api.EmojiStatus({
|
|
208
|
+
documentId: c.helpers.returnBigInt(String(r.documentId)),
|
|
209
|
+
...(r.until === undefined ? {} : { until: r.until }),
|
|
210
|
+
}),
|
|
211
|
+
})
|
|
212
|
+
),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const createInactive = ({ ask, guarded, plain, inactive }) => ({
|
|
216
|
+
inactiveChannels: plain(async (c) => ({
|
|
217
|
+
channels: (await inactive(c)).map(({ raw, date }) => ({
|
|
218
|
+
chat: idOf(raw),
|
|
219
|
+
title: raw.title ?? '',
|
|
220
|
+
date,
|
|
221
|
+
broadcast: Boolean(raw.broadcast),
|
|
222
|
+
})),
|
|
223
|
+
})),
|
|
224
|
+
leaveChannels: guarded(
|
|
225
|
+
(r) =>
|
|
226
|
+
Array.isArray(r.chats) &&
|
|
227
|
+
r.chats.length > 0 &&
|
|
228
|
+
r.chats.length <= MAX_LEAVE &&
|
|
229
|
+
r.chats.every((x) => CHANNEL.test(String(x))),
|
|
230
|
+
(r) => async (c) => {
|
|
231
|
+
const listed = new Map(
|
|
232
|
+
(await inactive(c)).map((x) => [idOf(x.raw), x.raw])
|
|
233
|
+
);
|
|
234
|
+
const left = [];
|
|
235
|
+
for (const chat of new Set(r.chats.map(String))) {
|
|
236
|
+
const raw = listed.get(chat);
|
|
237
|
+
if (raw) {
|
|
238
|
+
await ask(
|
|
239
|
+
c,
|
|
240
|
+
'leave',
|
|
241
|
+
new c.Api.channels.LeaveChannel({
|
|
242
|
+
channel: new c.Api.InputChannel({
|
|
243
|
+
channelId: raw.id,
|
|
244
|
+
accessHash: raw.accessHash,
|
|
245
|
+
}),
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
left.push(chat);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, left };
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
export const createChannelManage = (env, { userClient }) => {
|
|
257
|
+
const kit = kitOf(env, userClient);
|
|
258
|
+
return {
|
|
259
|
+
...createPublicLink(kit),
|
|
260
|
+
...createLook(env, kit),
|
|
261
|
+
...createInactive(kit),
|
|
262
|
+
};
|
|
263
|
+
};
|
|
@@ -24,6 +24,7 @@ import { settingsInfo } from './telegram-chat-settings.js';
|
|
|
24
24
|
import { birthdayOf, colorOf } from './telegram-profile-extras.js';
|
|
25
25
|
import { monoforumInfo } from './telegram-channel-dm-shape.js';
|
|
26
26
|
import { groupStickersInfo } from './telegram-group-extras.js';
|
|
27
|
+
import { statusOf as emojiStatusOf } from './telegram-emoji-status.js';
|
|
27
28
|
import { ALL_ADMIN_RIGHTS } from './telegram-admin-rights.js';
|
|
28
29
|
|
|
29
30
|
const CHAT = /^-?\d{1,20}$/;
|
|
@@ -166,6 +167,20 @@ const usernamesInfo = (entity) => {
|
|
|
166
167
|
: {};
|
|
167
168
|
};
|
|
168
169
|
|
|
170
|
+
// A channel's name colour, profile colour and emoji status, for those who
|
|
171
|
+
// may change them (R-Y216): the creator, or an admin with the change-info right.
|
|
172
|
+
const lookInfo = (entity) =>
|
|
173
|
+
entity.creator || entity.adminRights?.changeInfo
|
|
174
|
+
? {
|
|
175
|
+
canAppearance: true,
|
|
176
|
+
look: {
|
|
177
|
+
color: colorOf(entity.color),
|
|
178
|
+
profileColor: colorOf(entity.profileColor),
|
|
179
|
+
status: emojiStatusOf(entity.emojiStatus),
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
: {};
|
|
183
|
+
|
|
169
184
|
// What the user may do with administrators and the history new members see
|
|
170
185
|
// (R-Y204): their own rights (the creator has all), who may add admins, and
|
|
171
186
|
// whether the history is hidden from new members.
|
|
@@ -288,6 +303,7 @@ const fetchChannel = async (client, Api, inputPeer, peers) => {
|
|
|
288
303
|
...wallpaperOf(full),
|
|
289
304
|
...usernamesInfo(entity),
|
|
290
305
|
...collectibleOf(entity),
|
|
306
|
+
...lookInfo(entity),
|
|
291
307
|
...inviteOf(entity, full),
|
|
292
308
|
// The group's sticker set and emoji pack (R-Y211).
|
|
293
309
|
...groupStickersInfo(entity, full),
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - `leave` — a basic group: the user leaves it (messages.deleteChatUser)
|
|
13
13
|
* and its history goes; a supergroup or channel:
|
|
14
14
|
* channels.leaveChannel.
|
|
15
|
+
* - `destroy` — the owner deletes a group or channel for everyone
|
|
16
|
+
* (channels.deleteChannel, messages.deleteChat; R-Y216), no undo; the
|
|
17
|
+
* chat leaves the list like a left one.
|
|
15
18
|
*
|
|
16
19
|
* Telegram deletes history a part at a time (`offset` says there is
|
|
17
20
|
* more); it is asked again until done. The chat's stored messages are
|
|
@@ -25,7 +28,12 @@
|
|
|
25
28
|
import { markDeleted as tombstone } from '../storage/soft-delete.js';
|
|
26
29
|
|
|
27
30
|
const SOURCE = 'telegram';
|
|
28
|
-
export const REMOVAL_ACTIONS = Object.freeze([
|
|
31
|
+
export const REMOVAL_ACTIONS = Object.freeze([
|
|
32
|
+
'clear',
|
|
33
|
+
'delete',
|
|
34
|
+
'leave',
|
|
35
|
+
'destroy',
|
|
36
|
+
]);
|
|
29
37
|
const MAX_ROUNDS = 100;
|
|
30
38
|
|
|
31
39
|
const kindOf = (chat) => {
|
|
@@ -42,7 +50,7 @@ export const removalAllowed = (chat, action) => {
|
|
|
42
50
|
if (action === 'delete') {
|
|
43
51
|
return kind === 'personal';
|
|
44
52
|
}
|
|
45
|
-
if (action === 'leave') {
|
|
53
|
+
if (action === 'leave' || action === 'destroy') {
|
|
46
54
|
return kind !== 'personal';
|
|
47
55
|
}
|
|
48
56
|
return action === 'clear';
|
|
@@ -84,6 +92,9 @@ export const createChatRemoval = (env, { userClient, apply }) => {
|
|
|
84
92
|
};
|
|
85
93
|
|
|
86
94
|
const askChannel = async (client, Api, peer, { action, chat }) => {
|
|
95
|
+
if (action === 'destroy') {
|
|
96
|
+
return client.invoke(new Api.channels.DeleteChannel({ channel: peer }));
|
|
97
|
+
}
|
|
87
98
|
if (action === 'leave') {
|
|
88
99
|
return client.invoke(new Api.channels.LeaveChannel({ channel: peer }));
|
|
89
100
|
}
|
|
@@ -101,6 +112,14 @@ export const createChatRemoval = (env, { userClient, apply }) => {
|
|
|
101
112
|
if (kind === 'channel') {
|
|
102
113
|
return askChannel(client, Api, peer, { action, chat });
|
|
103
114
|
}
|
|
115
|
+
if (kind === 'group' && action === 'destroy') {
|
|
116
|
+
const { helpers } = await env.importer();
|
|
117
|
+
return client.invoke(
|
|
118
|
+
new Api.messages.DeleteChat({
|
|
119
|
+
chatId: helpers.returnBigInt(String(chat).slice(1)),
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
104
123
|
if (kind === 'group' && action === 'leave') {
|
|
105
124
|
const { helpers } = await env.importer();
|
|
106
125
|
const left = await client.invoke(
|