UNPKG

teleform

Version:

Format Telegram messages, escape special characters, switch formatting styles, convert special entities to formatted text and vice versa, form Telegram links, use Unicode symbols.

681 lines (680 loc) 27.9 kB
export { link }; class link { static joint = '-'; static https = true; static short = false; static proto = false; static #protocols = { tg: 'tg', http: 'http', https: 'https' }; static #laconic = 't.me'; static #verbose = 'telegram.me'; static operations = { start: 'start', attach: 'attach', startapp: 'startapp', startattach: 'startattach' }; static parameters = { mode: { blur: 'blur', motion: 'motion', compact: 'compact' }, admin: { change_info: 'change_info', post_messages: 'post_messages', edit_messages: 'edit_messages', delete_messages: 'delete_messages', restrict_members: 'restrict_members', invite_users: 'invite_users', pin_messages: 'pin_messages', manage_topics: 'manage_topics', promote_members: 'promote_members', manage_video_chats: 'manage_video_chats', anonymous: 'anonymous', manage_chat: 'manage_chat', post_stories: 'post_stories', edit_stories: 'edit_stories', delete_stories: 'delete_stories' }, choose: { users: 'users', bots: 'bots', groups: 'groups', channels: 'channels' } }; static #number_regexp = /^(\+)?(\d{4,15})$/; static #hex_color_regexp = /^([a-f0-9]{6})$/i; static #timestamp_regexps = { s: /^(\d+)$/, m: /^(\d+):(\d{1,2})$/, h: /^(?:(\d+)h)?(?:(\d{1,2})m)?(?:(\d{1,2})s)$/ }; static create(args) { this.#adjust(args); const { subject, element, entity, id, params = {}, user, launch, attach = false, short = this.short, proto = this.proto, from = undefined } = args ?? {}; return this.#base(short, proto) + (!subject ? '' : this.#path(user ?? subject, element) + this.#query(launch, attach, entity, id, params, user ? subject : null)); } static #adjust(args, fields = [], values = []) { if (args?.from == null) return; if (args?.subject == null) return; if (typeof args.subject !== 'string') args.subject = args.subject.toString(); if (['group', 'channel'].includes(args.from)) if (args.subject.startsWith('c/')) return args.subject = null; if (!this.#is_property_true(args.proto, 'proto')) { if (args.from === 'user') { const spot = args.subject.match(this.#number_regexp); if (spot && !spot[1]) args.subject = '+' + args.subject; } else if (args.from === 'app' && args.user) { if (typeof args.user !== 'string') args.user = args.user.toString(); const spot = args.user.match(this.#number_regexp); if (spot && !spot[1]) args.user = '+' + args.user; } } else { switch (args.from) { case 'user': { [fields, values] = ['domain', args.subject]; if (args.from === 'user') { const spot = args.subject.match(this.#number_regexp); if (spot) [fields, values] = ['phone', spot[2]]; } args.subject = 'resolve'; break; } case 'contact': case 'folder': case 'stickers': case 'emojis': case 'theme': case 'login': case 'language': { if (args.from === 'login' && !args.element) { delete args.params?.code; break; } fields = { contact: 'token', folder: 'slug', stickers: 'set', emojis: 'set', theme: 'slug', login: 'code', language: 'lang' }[args.from]; values = args.element; args.element = null; break; } case 'invite': { fields = 'invite'; values = args.subject.replace(/^(\+|joinchat\/)/i, ''); args.subject = 'join'; break; } case 'group': case 'channel': case 'videochat': case 'livestream': case 'voicechat': case 'story': case 'bot': case 'app': case 'game': { fields = ['domain']; values = [args.subject]; args.subject = 'resolve'; if (args.from === 'story') { fields.push('story'); values.push(args.element.split('/')[1]); } else if (args.from === 'app') { if (args.element) { fields.push('appname'); values.push(args.element); } if (args.user) { const value0 = values[0]; if (typeof args.user !== 'string') args.user = args.user.toString(); const spot = args.user.match(this.#number_regexp); if (spot) { fields[0] = 'phone'; values[0] = spot[2]; } else values[0] = args.user; fields.push('attach'); values.push(value0); delete args.user; } } args.element = null; break; } case 'message': { if (args.subject.startsWith('c/')) { fields.push('channel'); values.push(args.subject.split('/')[1]); args.subject = 'privatepost'; } else { fields.push('domain'); values.push(args.subject); args.subject = 'resolve'; } let elements = null; if (args.element) { elements = args.element.split('/'); fields.push('post'); values.push(elements[1] ?? elements[0]); } if (args.params?.single !== undefined) { fields.push('single'); values.push(null); delete args.params?.single; } if (elements && elements[1]) { fields.push('thread'); values.push(elements[0]); } args.element = null; break; } case 'share': { args.subject = 'msg_url'; break; } case 'boost': { let subj, priv; const subjects = args.subject.split('/'); if (args.element) [subj, priv] = [args.element, false]; else if (subjects.length > 1) [subj, priv] = [subjects[1], true]; else if (args.params?.c) [subj, priv] = [args.params?.c, true]; else [subj, priv] = [args.subject, false]; fields = priv ? 'channel' : 'domain'; values = subj; args.subject = 'boost'; args.element = null; if (args.params) { args.params.c = undefined; args.params.boost = undefined; } // causes error // args.params?.c = args.params?.boost = undefined break; } case 'invoice': { fields = 'slug'; values = args.element ?? args.subject.substring(1); args.subject = 'invoice'; args.element = null; break; } } if (!Array.isArray(fields)) [fields, values] = [[fields], [values]]; const list = fields.map((f, i) => [f, values[i]]); args.params = { ...Object.fromEntries(list), ...args.params }; } } static #base(short, proto) { if (proto) return this.#protocols.tg + ':' + (short ? '' : '//'); else { let scheme; switch (this.https) { default: scheme = ''; break; case true: scheme = this.#protocols.https; break; case false: scheme = this.#protocols.http; break; } return scheme + (scheme ? '://' : '') + (short ? this.#laconic : this.#verbose) + '/'; } } static #path(subject, element) { return subject ? element ? `${subject}/${element}` : subject : ''; } static #query(launch, attach, entity, id, params, botapp) { const starting = { command: this.#start_command(launch ?? !!entity, attach, params), param: this.#start_param(entity, id) }; for (const i in params) if (params[i] === undefined || (Array.isArray(params[i]) && !params[i].length)) delete params[i]; const pc = structuredClone(params); delete params?.domain; delete params?.phone; delete params?.appname; delete params?.attach; const parameters = { ...(!pc?.domain ? {} : { domain: pc?.domain }), ...(!pc?.phone ? {} : { phone: pc?.phone }), ...(!pc?.appname ? {} : { appname: pc?.appname }), ...(!pc?.attach ? {} : { attach: pc?.attach }), ...(!botapp ? {} : { [this.operations.attach]: botapp }), ...(!starting.command ? {} : { [starting.command]: starting.param }), ...params }; if (Object.keys(parameters).length === 0) return ''; return '?' + Object.entries(parameters).map(([key, value]) => { const field = encodeURIComponent(key); if (value === null) return field; if (Array.isArray(value)) value = value.map(encodeURIComponent).join('+'); else if (typeof value === 'object') value = encodeURIComponent(JSON.stringify(value)); else if (['t'].includes(key)) undefined; else value = encodeURIComponent(value); return `${field}=${value}`; }).join('&'); } static #start_command(launch, attach, params) { switch (true) { case typeof launch === 'string': return launch; case attach: case !!params?.choose: return this.operations.startattach; case launch: return this.operations[`start${attach === null ? '' : 'app'}`]; } } static #start_param(entity, id) { return entity ? id ? entity + this.joint + id : entity : null; } static social(args) { const { did, mid, cid, tid, single, mt, mt_to, modern = true, short = this.short, proto = this.proto, from = undefined } = args ?? {}; if (!did) return this.create(); const subject = this.#resolve_subject(did); const element = mid ? (tid && modern ? `${tid}/` : '') + mid : null; const media_timestamp = mt_to ? this.#convert_timestamp(mt.toString(), mt_to) : mt; const params = { ...(single ? { single: null } : {}), ...(tid && (!modern || !mid) ? { thread: tid } : {}), ...(cid ? { comment: cid } : {}), ...(media_timestamp ? { t: media_timestamp } : {}) }; return this.create({ subject, element, params, short, proto, from }); } static background(args) { const { slug, colors, intensity, rotation, mode, short = this.short, proto = this.proto, from = undefined } = args ?? {}; let [element, params] = [null, { intensity, rotation, mode }]; const cn = colors ? colors.length : 0; const bg_color = colors ? colors.join(cn < 3 ? '-' : '~') : colors; if (proto) { if (slug && cn) params = { bg_color: bg_color, ...params }; else if (cn === 1) params = { color: bg_color, ...params }; else if (cn > 1) params = { gradient: bg_color, ...params }; if (slug) params = { slug: slug, ...params }; } else { element = slug ?? bg_color; if (slug) params = { bg_color: bg_color, ...params }; } return this.create({ subject: 'bg', element, params, short, proto, from }); } static settings(type, short = this.short) { return this.create({ subject: 'settings', element: type, short, proto: true }); } static #is_property_true(value, field) { return value || ((value === undefined || value === null) && this[field]); } static #resolve_subject(dialog_id) { return this.#is_natural_numeric(dialog_id) ? `c/${dialog_id}` : dialog_id; } static #is_natural_numeric(data) { return /^\d+$/.test(data); } static #is_integer_numeric(data) { return /^(\+|-)?\d+$/.test(data); } static #integer_numeric(data) { if (!this.#is_number_or_string(data)) return; if (this.#is_integer_numeric(data)) return data; else return this.#extract_integer(data.toString()); } static #is_number_or_string(data) { return ['number', 'string'].includes(typeof data); } static #extract_integer(data) { if (/^(\+|-)?\d+\.\d*$/.test(data)) return data.split('.')[0]; } static #convert_timestamp(ts, to) { const us = Object.keys(this.#timestamp_regexps); if (!us.includes(to)) return ts; let time_spot; for (const unit of us) if (time_spot = ts.match(this.#timestamp_regexps[unit])) break; if (!time_spot) return ts; let [h, m, s] = [ time_spot[3] ? time_spot[1] ?? 0 : 0, time_spot[3] ? time_spot[2] ?? 0 : time_spot[2] ? time_spot[1] : 0, time_spot[3] ?? time_spot[2] ?? time_spot[1] ]; const time = h * 3600 + m * 60 + +s; if (to === 's') return `${time}`; [h, m, s] = this.#get_hms(time); if (to === 'm') return `${(h * 60 + +m).toString().padStart(2, '0')}:${s.padStart(2, '0')}`; return `${+h ? `${h}h` : ''}${+m ? `${m.padStart(2, '0')}m` : ''}${s.padStart(2, '0')}s`; } static #get_hms(seconds) { const hours = Math.floor(seconds / 3600); seconds %= 3600; const minutes = Math.floor(seconds / 60); return [hours, minutes, seconds % 60].map(value => value.toString()); } static #extract_wallpaper_params(args, slug = null) { let params, short, proto, rotation, intensity, mode, mode_number = 1; args = Object.values(args); if (slug) args = args.slice(1); const is_first_array = Array.isArray(args[0]); let [colors, rest] = is_first_array ? [ args[0], args.slice(1) ] : [structuredClone(args), structuredClone(args)]; if (typeof colors[0] === 'boolean' || (is_first_array && Object.values(this.parameters.mode).includes(colors[0]))) { colors = []; } else { for (let i = 0; i < colors.length + 1; i++) { if (!this.#hex_color_regexp.test(colors[i] ?? '')) { if (!is_first_array) { colors = colors.slice(0, i); rest = rest.slice(i); break; } else colors.splice(i, 1); } } } if (colors.length === 0) { colors = undefined; if (is_first_array) rest = args; mode_number = 2; } if (!slug) mode_number = 0; else if (colors) intensity = this.#intensity(rest); if (colors?.length === 2) rotation = this.#rotation(rest); mode = Array.isArray(rest[0]) ? rest.shift() : rest.slice(0, mode_number).map((_, i) => { if (this.#extract_boolean(rest)) if (mode_number === 1 || i === 1) return this.parameters.mode.motion; else return this.parameters.mode.blur; }).filter(v => v !== undefined); short = this.#extract_boolean(rest); proto = this.#extract_boolean(rest); params = { intensity, rotation, mode }; return [slug, { colors: colors, ...params }, short, proto]; } static #extract_numeric(rest) { return this.#integer_numeric(rest.shift()); } static #extract_boolean(rest) { return typeof rest[0] === 'boolean' ? rest.shift() : undefined; } static #rotation(rest) { const result = this.#extract_numeric(rest); if (result !== undefined) return this.#adjust_rotation(result); } static #intensity(rest) { const result = this.#extract_numeric(rest); if (result !== undefined) return this.#adjust_intensity(result); } static #adjust_rotation(rotation, by = 45) { return rotation < 0 ? 0 : (Math.round(rotation / by) * by) % 360; } static #adjust_intensity(intensity) { return intensity > 100 ? 100 : intensity < -100 ? -100 : intensity; } static user(did, short, proto) { let subject, params = {}, from = 'user'; if (typeof did !== 'string') did = did.toString(); if (did.match(/^\d+$/)) { subject = 'user'; params = { id: did }; proto = true; from = undefined; } else subject = did; return this.create({ subject, params, short, proto, from }); } static contact(token, short, proto) { return this.create({ subject: 'contact', element: token, short, proto, from: 'contact' }); } static invite(hash, modern = true, short, proto) { const prefix = modern ? '+' : 'joinchat/'; return this.create({ subject: prefix + hash, short, proto, from: 'invite' }); } static folder(slug, short, proto) { return this.create({ subject: 'addlist', element: slug, short, proto, from: 'folder' }); } static group(did, short, proto) { return this.social({ did, short, proto, from: 'group' }); } static channel(did, short, proto) { return this.social({ did, short, proto, from: 'channel' }); } static forum(did, mid, tid, modern, short, proto) { return this.message(did, mid, null, tid, null, null, null, modern, short, proto); } static message(did, mid, cid, tid, single, mt, mt_to, modern, short, proto) { return this.social({ did, mid, cid, tid, single, mt, mt_to, modern, short, proto, from: 'message' }); } static share(url, text, short, proto) { return this.create({ subject: 'share', params: { url, text }, short, proto, from: 'share' }); } static videochat(did, invite_hash, short, proto) { const subject = this.#resolve_subject(did); return this.create({ subject, params: { videochat: invite_hash ?? null }, short, proto, from: 'videochat' }); } static livestream(did, invite_hash, short, proto) { const subject = this.#resolve_subject(did); return this.create({ subject, params: { livestream: invite_hash ?? null }, short, proto, from: 'livestream' }); } static voicechat(did, invite_hash, short, proto) { const subject = this.#resolve_subject(did); return this.create({ subject, params: { voicechat: invite_hash ?? null }, short, proto, from: 'voicechat' }); } static stickers(slug, short, proto) { return this.create({ subject: 'addstickers', element: slug, short, proto, from: 'stickers' }); } static emoji(id, short) { return this.create({ subject: 'emoji', params: { id }, short, proto: true }); } static emojis(slug, short, proto) { return this.create({ subject: 'addemoji', element: slug, short, proto, from: 'emojis' }); } static story(did, sid, short, proto) { const subject = this.#resolve_subject(did); return this.create({ subject, element: `s/${sid}`, short, proto, from: 'story' }); } static boost(did, modern = true, short, proto) { const rs = this.#resolve_subject(did); let subject, element = '', params = {}; if (rs === did) // public if (modern) [subject, element] = ['boost', did]; else [subject, params] = [did, { boost: null }]; else if (modern) [subject, params] = [rs, { boost: null }]; else [subject, params] = ['boost', { c: did }]; return this.create({ subject, element, params, short, proto, from: 'boost' }); } static proxy(server, port, secret, short, proto) { const params = { server, port, secret }; return this.create({ subject: 'proxy', params, short, proto, from: 'proxy' }); } static socks5(...args) { return this.socks5proxy(...args); } static socks5proxy(server, port, user, pass, short, proto) { const params = { server, port, user, pass }; return this.create({ subject: 'socks', params, short, proto, from: 'socks5proxy' }); } static theme(name, short, proto) { return this.create({ subject: 'addtheme', element: name, short, proto, from: 'theme' }); } static bg(...args) { return this.wallpaper(...args); } static wallpaper(arg0, arg1, arg2, arg3, arg4, arg5, arg6, short, proto) { let element, params = {}; if (Array.isArray(arg0) || this.#hex_color_regexp.test(arg0)) [element, params, short, proto] = this.#extract_wallpaper_params(arguments); else [element, params, short, proto] = this.#extract_wallpaper_params(arguments, arg0); return this.background({ slug: element, ...params, short, proto, from: 'wallpaper' }); } static bot(bot, entity, id, startgroup, startchannel, admin, short, proto) { if (startgroup === true) [startchannel, admin, short, proto] = [false, startchannel, admin, short]; else if (entity === true) [entity, id, startgroup, startchannel, admin, short, proto] = [ null, null, false, true, Array.isArray(id) ? id : Object.values(this.parameters.admin), startgroup, startchannel ]; const launch = startgroup ? 'startgroup' : startchannel ? 'startchannel' : null; if (startchannel) entity = id = null; else if (typeof admin === 'string') admin = [admin]; return this.create({ subject: bot, entity, id, params: { admin }, launch, attach: null, short, proto, from: 'bot' }); } static game(bot, game, short, proto) { return this.create({ subject: bot, params: { game: game }, short, proto, from: 'game' }); } static change_number(short) { return this.settings('change_number', short); } static devices(short) { return this.settings('devices', short); } static folders(short) { return this.settings('folders', short); } static languages(short) { return this.settings('language', short); } static privacy(short) { return this.settings('privacy', short); } static auto_delete(short) { return this.settings('auto_delete', short); } static edit_profile(short) { return this.settings('edit_profile', short); } static themes(short) { return this.settings('themes', short); } static login(code, token, short, proto) { if (!code && token) proto = true; const [element, params] = proto ? [null, { code, token }] : [code, {}]; return this.create({ subject: 'login', element, params, short, proto, from: 'login' }); } static invoice(slug, modern = true, short, proto) { const [subject, element] = modern ? [`\$${slug}`, null] : ['invoice', slug]; return this.create({ subject, element, short, proto, from: 'invoice' }); } static language(slug, short, proto) { return this.create({ subject: 'setlanguage', element: slug, short, proto, from: 'language' }); } static passport(params, modern = true, short) { if (!modern) params = { domain: 'telegrampassport', ...params }; return this.create({ subject: modern ? 'passport' : 'resolve', params, short, proto: true }); } static confirm_phone(phone, hash, short, proto) { if (typeof phone !== 'string') phone = phone.toString(); if (this.#number_regexp.test(phone)) phone = phone.substring(1); return this.create({ subject: 'confirmphone', params: { phone, hash }, short, proto }); } static premium(type, data, short, proto = this.proto) { type = ([ null, 'giftcode', 'offer', 'multigift' ][type]) ?? (type === true ? 'giftcode' : type); if (type.startsWith('premium_')) type = type.split('_')[1]; const is_giftcode = type === 'giftcode'; if (!is_giftcode) proto = true; const subject = is_giftcode ? type : `premium_${type}`; const [element, params] = !proto ? [data, null] : [null, { [is_giftcode ? 'slug' : 'ref']: data }]; return this.create({ subject, element, params, short, proto }); } static app(bot, app, entity, id, compact = false, user, choose, launch, attach = false, short, proto) { if (app === true) [app, attach] = [null, true]; if (Array.isArray(user)) [user, choose] = [null, user]; const params = { ...(compact ? { mode: this.parameters.mode.compact } : {}), ...(choose ? { choose } : {}) }; if (typeof launch !== 'boolean' && !attach && !app) launch = entity || !user ? this.operations[user ? 'startattach' : 'startapp'] : null; return this.create({ subject: bot, element: app, entity, id, params, user, launch, attach, short, proto, from: 'app' }); } }