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.
Files changed (57) hide show
  1. package/js/src/server/routes-telegram-channel-manage.js +79 -0
  2. package/js/src/server/routes-telegram-emoji-groups.js +20 -0
  3. package/js/src/server/routes-telegram-storage.js +73 -0
  4. package/js/src/server/routes-telegram-translate-chat.js +31 -0
  5. package/js/src/server/routes-telegram-yextras.js +17 -0
  6. package/js/src/server/routes-telegram.js +2 -0
  7. package/js/src/sources/telegram-actions.js +17 -2
  8. package/js/src/sources/telegram-channel-manage.js +263 -0
  9. package/js/src/sources/telegram-chat-info.js +16 -0
  10. package/js/src/sources/telegram-chat-removal.js +21 -2
  11. package/js/src/sources/telegram-emoji-groups.js +95 -0
  12. package/js/src/sources/telegram-format.js +39 -4
  13. package/js/src/sources/telegram-notify-extras.js +144 -0
  14. package/js/src/sources/telegram-stickers.js +6 -1
  15. package/js/src/sources/telegram-storage.js +380 -0
  16. package/js/src/sources/telegram-translate-chat.js +57 -0
  17. package/js/src/sources/telegram-translate.js +5 -5
  18. package/js/src/web/app.css +157 -2
  19. package/js/src/web/app.js +2 -0
  20. package/js/src/web/auto-download.js +37 -0
  21. package/js/src/web/chat-actions.js +1 -0
  22. package/js/src/web/chat-channel-limit.js +142 -0
  23. package/js/src/web/chat-edit.js +3 -0
  24. package/js/src/web/chat-emoji-custom.js +338 -0
  25. package/js/src/web/chat-emoji-status.js +21 -9
  26. package/js/src/web/chat-emoji-tools.js +17 -1
  27. package/js/src/web/chat-emoji.js +214 -72
  28. package/js/src/web/chat-group-stickers.js +3 -10
  29. package/js/src/web/chat-invite.js +10 -0
  30. package/js/src/web/chat-manage.js +303 -0
  31. package/js/src/web/chat-message.js +16 -8
  32. package/js/src/web/chat-profile-extras.js +3 -3
  33. package/js/src/web/chat-public-link.js +314 -0
  34. package/js/src/web/chat-selection.js +2 -1
  35. package/js/src/web/chat-similar.js +14 -1
  36. package/js/src/web/chat-translate-bar.js +293 -0
  37. package/js/src/web/chat-translate.js +93 -27
  38. package/js/src/web/chat-translation-switch.js +20 -22
  39. package/js/src/web/chat-view.js +3 -0
  40. package/js/src/web/dom-telegram-calls.js +27 -0
  41. package/js/src/web/locales/en-chat-account.js +105 -0
  42. package/js/src/web/locales/hi-chat-account.js +106 -0
  43. package/js/src/web/locales/ru-chat-account.js +106 -0
  44. package/js/src/web/locales/zh-chat-account.js +99 -0
  45. package/js/src/web/settings-confirm.js +62 -0
  46. package/js/src/web/settings-fields.js +47 -0
  47. package/js/src/web/settings-media.js +21 -1
  48. package/js/src/web/settings-notify-extras.js +160 -0
  49. package/js/src/web/settings-notify.js +16 -4
  50. package/js/src/web/settings-storage.js +235 -0
  51. package/js/src/web/settings-view.js +5 -1
  52. package/js/src/web/translate-detect.js +108 -0
  53. package/js/src/web/translate-store.js +140 -0
  54. package/js/src/web/tutorial-clear.js +197 -0
  55. package/js/src/web/tutorial.js +48 -4
  56. package/js/src/web/use-redraw.js +14 -0
  57. 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(['clear', 'delete', 'leave']);
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(