Add event-driven Telegram Web K chat profiles for Chrome

This commit is contained in:
agrynco committed 2026-11-06 10:26:42 +02:00
1 parent 15197da548
commit c18148a574
8 files changed
+197 -6

No files matched your search

+8
View File
@@ -317,6 +317,14 @@ Telegram ігнорується. Teams підтримує зазначений
Профілі локальні, входять до ручного експорту/імпорту, але не до Профілі локальні, входять до ручного експорту/імпорту, але не до
хмарної синхронізації. Доступно з v0.6.48. хмарної синхронізації. Доступно з v0.6.48.
### Telegram Web in Chrome — development build
Open a chat in Telegram Web K (`https://web.telegram.org/k/`) in Chrome, then use **Settings → Languages → Languages for a chat...** and select **Telegram Web: name**. Choose languages, then **Use languages → Save**. The same subset applies to Alt+Shift, Win+Space, Win+Shift+Space and Next layout. Web profiles are separate from desktop Telegram profiles. Same-name chats across browser profiles/accounts share a web profile.
Recognition uses Chrome accessibility: the address bar and active document must both identify a Telegram chat, then the visible header supplies the name. The app watches foreground/focus and address/header-change events; it does not use OCR, a polling timer, an extension, or message-history extraction. The address bar, home page and unsupported URLs do not match. Web A and other browsers have not been validated and are not advertised as supported. Page or accessibility changes can make a chat unavailable instead of guessing.
**Українською.** У дев-версії підтримано Telegram Web K у Chrome. Відкрий чат, потім **Settings → Languages → Languages for a chat...**, вибери **Telegram Web: ім’я**, мови та **Use languages → Save**. Набір діє для Alt+Shift, Win+Space, Win+Shift+Space і Next layout. Вебпрофілі окремі від Telegram Desktop; однакові назви в різних акаунтах/профілях браузера мають спільний вебпрофіль. Адреса й назва беруться через accessibility, без OCR, таймера, розширення та читання історії повідомлень. В адресному рядку, на головній сторінці й для непідтримуваних адрес профіль не діє. Web A та інші браузери не перевірені й не заявлені як підтримувані.
### Existing features ### Existing features
- Installed Windows keyboard layouts, configurable cycle/order and direct shortcuts; IMEs are switch-only. - Installed Windows keyboard layouts, configurable cycle/order and direct shortcuts; IMEs are switch-only.
+4
View File
@@ -559,6 +559,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot, HWND ser
} }
App::~App() App::~App()
{ {
stopBrowserChatMonitor();
if (auto host = historyController(historyProcess)) PostMessageW(host, WM_CLOSE, 0, 0); if (auto host = historyController(historyProcess)) PostMessageW(host, WM_CLOSE, 0, 0);
if (historyProcess) CloseHandle(historyProcess); if (historyProcess) CloseHandle(historyProcess);
if (notificationRegistration) CloseHandle(notificationRegistration); if (notificationRegistration) CloseHandle(notificationRegistration);
@@ -978,6 +979,7 @@ void App::switchChatLayout(bool reverse)
{ {
struct Reset { std::atomic<bool> &busy; ~Reset() { busy = false; } } reset{busy}; struct Reset { std::atomic<bool> &busy; ~Reset() { busy = false; } } reset{busy};
auto valid = [&] { return GetForegroundWindow() == target && windowChatKey(target) == key; }; auto valid = [&] { return GetForegroundWindow() == target && windowChatKey(target) == key; };
if (key.starts_with(L"telegram-web\n") && readChromeChat(target) != key) return;
if (!valid()) return; if (!valid()) return;
auto next = nextChatLayout(cycle, available, windowLayout(target), reverse); auto next = nextChatLayout(cycle, available, windowLayout(target), reverse);
if (next) switchWindowLayout(target, next, valid); if (next) switchWindowLayout(target, next, valid);
@@ -1275,6 +1277,7 @@ void App::transform(int command, int snippet)
} }
} reset{busy, window, preDeferred}; } reset{busy, window, preDeferred};
auto effectiveChat = requestedChat; auto effectiveChat = requestedChat;
if (lower(processName(target)) == L"chrome") effectiveChat = readChromeChat(target);
config.cycle = chatCycle(config, effectiveChat, available); config.cycle = chatCycle(config, effectiveChat, available);
auto start = GetTickCount64(); auto start = GetTickCount64();
std::wstring reason = L"cancelled-before-selection"; std::wstring reason = L"cancelled-before-selection";
@@ -2318,6 +2321,7 @@ int App::execute()
// Resume pending OCR, including older images without a saved OCR state. // Resume pending OCR, including older images without a saved OCR state.
scheduleHistoryOcr(settings); scheduleHistoryOcr(settings);
addTray(); addTray();
if (!smoke) startBrowserChatMonitor();
SetTimer(window, 1, 60000, nullptr); SetTimer(window, 1, 60000, nullptr);
if (!smoke) SetTimer(window, 6, 1000, nullptr); if (!smoke) SetTimer(window, 6, 1000, nullptr);
SetThreadExecutionState(ES_CONTINUOUS | SetThreadExecutionState(ES_CONTINUOUS |
+143
View File
@@ -0,0 +1,143 @@
// Chrome Telegram Web integration: native accessibility, no OCR or polling timer.
#include "chat_context.h"
#include <uiautomation.h>
#include <atomic>
#include <thread>
namespace ks {
namespace {
template<class T> struct BrowserCom {
T *p{}; ~BrowserCom(){if(p)p->Release();}
T** out(){return &p;} T* operator->(){return p;}
};
std::wstring browserString(IUIAutomationElement* e, PROPERTYID id) {
VARIANT v;VariantInit(&v);std::wstring s;
if(SUCCEEDED(e->GetCurrentPropertyValue(id,&v)) && v.vt==VT_BSTR && v.bstrVal) s.assign(v.bstrVal,SysStringLen(v.bstrVal));
VariantClear(&v);return s;
}
struct ChromeElements { BrowserCom<IUIAutomationElement> address,document,header; int addresses{},documents{}; };
void findChromeElements(IUIAutomationTreeWalker* walker,IUIAutomationElement* e,ChromeElements& found,int depth,int& budget) {
if(depth>12 || --budget<0)return;
CONTROLTYPEID type{};BOOL offscreen=TRUE;e->get_CurrentControlType(&type);e->get_CurrentIsOffscreen(&offscreen);
if(type==UIA_DocumentControlTypeId){
if(!offscreen && browserString(e,UIA_AutomationIdPropertyId)==L"RootWebArea"){
if(++found.documents==1){e->AddRef();found.document.p=e;}}
return; // Never traverse message content.
}
if(type==UIA_EditControlTypeId && !offscreen && browserString(e,UIA_ClassNamePropertyId)==L"OmniboxViewViews"){
if(++found.addresses==1){e->AddRef();found.address.p=e;}}
BrowserCom<IUIAutomationElement> child;walker->GetFirstChildElement(e,child.out());
while(child.p && budget>0){findChromeElements(walker,child.p,found,depth+1,budget);BrowserCom<IUIAutomationElement> next;walker->GetNextSiblingElement(child.p,next.out());child.p->Release();child.p=next.p;next.p=nullptr;}
}
IUIAutomationElement* chromeHeader(IUIAutomationTreeWalker* walker,IUIAutomationElement* document) {
// Web K's center pane is separate from its chat list and message history.
std::function<IUIAutomationElement*(IUIAutomationElement*,int)> center;
center=[&](IUIAutomationElement* e,int depth)->IUIAutomationElement* {
if(depth>3)return nullptr;
if(browserString(e,UIA_AutomationIdPropertyId)==L"column-center"){e->AddRef();return e;}
auto id=browserString(e,UIA_AutomationIdPropertyId);if(id==L"column-left"||id==L"column-right")return nullptr;
BrowserCom<IUIAutomationElement> c;walker->GetFirstChildElement(e,c.out());int limit=12;
while(c.p && --limit>0){if(auto result=center(c.p,depth+1))return result;BrowserCom<IUIAutomationElement> next;walker->GetNextSiblingElement(c.p,next.out());c.p->Release();c.p=next.p;next.p=nullptr;}return nullptr;
};
BrowserCom<IUIAutomationElement> pane;pane.p=center(document,0);if(!pane.p)return nullptr;
BrowserCom<IUIAutomationElement> header;walker->GetFirstChildElement(pane.p,header.out());if(!header.p)return nullptr;
BrowserCom<IUIAutomationElement> c;walker->GetFirstChildElement(header.p,c.out());int limit=16,buttons=0;IUIAutomationElement* title{};
while(c.p && --limit>0){CONTROLTYPEID type{};c->get_CurrentControlType(&type);if(type==UIA_ButtonControlTypeId)++buttons;
if(type==UIA_TextControlTypeId && !title){title=c.p;title->AddRef();}
BrowserCom<IUIAutomationElement> next;walker->GetNextSiblingElement(c.p,next.out());c.p->Release();c.p=next.p;next.p=nullptr;}
if(buttons<2 && title){title->Release();title=nullptr;}return title;
}
std::wstring chromeTitleKey(IUIAutomationElement* header){
if(!header)return {};
auto name=browserString(header,UIA_NamePropertyId);
if(name.empty()||name.size()>512||name.find_first_of(L"\r\n")!=name.npos)return {};
return L"telegram-web\n"+name;
}
std::wstring chromeSnapshot(IUIAutomation* uia,HWND window,ChromeElements& found) {
if(!IsWindow(window)||IsIconic(window)||lower(processName(window))!=L"chrome")return {};
BrowserCom<IUIAutomationElement> root;BrowserCom<IUIAutomationTreeWalker> walker;
if(FAILED(uia->ElementFromHandle(window,root.out()))||!root.p||FAILED(uia->get_ControlViewWalker(walker.out())))return {};
int budget=250;findChromeElements(walker.p,root.p,found,0,budget);
if(found.addresses!=1||found.documents!=1)return {};
auto address=telegramWebKey(browserString(found.address.p,UIA_ValueValuePropertyId));
auto document=telegramWebKey(browserString(found.document.p,UIA_ValueValuePropertyId));
BOOL editing=FALSE;found.address->get_CurrentHasKeyboardFocus(&editing);
if(editing || address.empty() || address!=document)return {};
found.header.p=chromeHeader(walker.p,found.document.p);
return chromeTitleKey(found.header.p);
}
std::mutex browserMutex;
HWND browserWindow{};std::wstring browserKey;
std::atomic<DWORD> browserThreadId{};
std::atomic<bool> browserPending{}, browserStopping{};
std::thread browserThread;
void invalidateBrowser(){std::lock_guard lock(browserMutex);browserKey.clear();}
void requestBrowser(){invalidateBrowser();auto id=browserThreadId.load();if(id && !browserPending.exchange(true))PostThreadMessageW(id,WM_APP+61,0,0);}
void CALLBACK browserForeground(HWINEVENTHOOK,DWORD,HWND,LONG,LONG,DWORD,DWORD){requestBrowser();}
class BrowserChanged final: public IUIAutomationPropertyChangedEventHandler {
std::atomic<ULONG> refs{1};
public:
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,void** out) override {if(!out)return E_POINTER;*out=nullptr;if(iid==IID_IUnknown||iid==IID_IUIAutomationPropertyChangedEventHandler){*out=static_cast<IUIAutomationPropertyChangedEventHandler*>(this);AddRef();return S_OK;}return E_NOINTERFACE;}
ULONG STDMETHODCALLTYPE AddRef() override{return ++refs;}
ULONG STDMETHODCALLTYPE Release() override{auto n=--refs;if(!n)delete this;return n;}
HRESULT STDMETHODCALLTYPE HandlePropertyChangedEvent(IUIAutomationElement*,PROPERTYID,VARIANT) override {requestBrowser();return S_OK;}
};
}
std::wstring readChromeChat(HWND window) {
auto hr=CoInitializeEx(nullptr,COINIT_MULTITHREADED);
struct Apartment{bool own;~Apartment(){if(own)CoUninitialize();}} apartment{SUCCEEDED(hr)};
BrowserCom<IUIAutomation2> uia;
if(FAILED(CoCreateInstance(CLSID_CUIAutomation8,nullptr,CLSCTX_INPROC_SERVER,IID_IUIAutomation2,(void**)uia.out())))return {};
uia->put_ConnectionTimeout(150);uia->put_TransactionTimeout(150);
ChromeElements elements;return chromeSnapshot(uia.p,window,elements);
}
std::wstring cachedChromeChat(HWND window){std::lock_guard lock(browserMutex);return window==browserWindow?browserKey:L"";}
void startBrowserChatMonitor(){
if(browserThread.joinable())return;
browserStopping=false;
browserThread=std::thread([] {
CoInitializeEx(nullptr,COINIT_MULTITHREADED);MSG message{};PeekMessageW(&message,nullptr,0,0,PM_NOREMOVE);
browserThreadId=GetCurrentThreadId();
auto foreground=SetWinEventHook(EVENT_SYSTEM_FOREGROUND,EVENT_SYSTEM_FOREGROUND,nullptr,browserForeground,0,0,WINEVENT_OUTOFCONTEXT);
// Focus changes cover tab selection and entering/leaving the address bar.
auto focus=SetWinEventHook(EVENT_OBJECT_FOCUS,EVENT_OBJECT_FOCUS,nullptr,browserForeground,0,0,WINEVENT_OUTOFCONTEXT);
BrowserCom<IUIAutomation2> uia;CoCreateInstance(CLSID_CUIAutomation8,nullptr,CLSCTX_INPROC_SERVER,IID_IUIAutomation2,(void**)uia.out());
BrowserCom<IUIAutomationElement> address,document,header;BrowserCom<BrowserChanged> handler;handler.p=new BrowserChanged;
if(uia.p){uia->put_ConnectionTimeout(150);uia->put_TransactionTimeout(150);}
requestBrowser();
if(browserStopping)PostThreadMessageW(GetCurrentThreadId(),WM_QUIT,0,0);
while(GetMessageW(&message,nullptr,0,0)>0){
if(message.message!=WM_APP+61){TranslateMessage(&message);DispatchMessageW(&message);continue;}
browserPending=false;
if(!uia.p)continue;
if(address.p){uia->RemovePropertyChangedEventHandler(address.p,handler.p);address.p->Release();address.p=nullptr;}
if(document.p){uia->RemovePropertyChangedEventHandler(document.p,handler.p);document.p->Release();document.p=nullptr;}
if(header.p){uia->RemovePropertyChangedEventHandler(header.p,handler.p);header.p->Release();header.p=nullptr;}
auto window=GetForegroundWindow();ChromeElements elements;auto key=chromeSnapshot(uia.p,window,elements);
PROPERTYID properties[]={UIA_ValueValuePropertyId};
bool subscribed=elements.address.p && elements.document.p;
if(subscribed){address.p=elements.address.p;elements.address.p=nullptr;document.p=elements.document.p;elements.document.p=nullptr;
subscribed=SUCCEEDED(uia->AddPropertyChangedEventHandlerNativeArray(address.p,TreeScope_Element,nullptr,handler.p,properties,1));
subscribed=SUCCEEDED(uia->AddPropertyChangedEventHandlerNativeArray(document.p,TreeScope_Element,nullptr,handler.p,properties,1)) && subscribed;
if(elements.header.p){header.p=elements.header.p;elements.header.p=nullptr;PROPERTYID nameProperty=UIA_NamePropertyId;
subscribed=SUCCEEDED(uia->AddPropertyChangedEventHandlerNativeArray(header.p,TreeScope_Element,nullptr,handler.p,&nameProperty,1)) && subscribed;}
// Close the read/subscribe race without scanning the content tree.
key=telegramWebKey(browserString(address.p,UIA_ValueValuePropertyId));
if(key.empty() || key!=telegramWebKey(browserString(document.p,UIA_ValueValuePropertyId)))key.clear();
else key=chromeTitleKey(header.p);
BOOL editing=FALSE;address->get_CurrentHasKeyboardFocus(&editing);if(editing)key.clear();
}
std::lock_guard lock(browserMutex);browserWindow=window;browserKey=subscribed && !browserPending && window==GetForegroundWindow()?key:L"";
}
browserThreadId=0;
if(foreground)UnhookWinEvent(foreground);
if(focus)UnhookWinEvent(focus);
if(uia.p){if(header.p)uia->RemovePropertyChangedEventHandler(header.p,handler.p);if(address.p)uia->RemovePropertyChangedEventHandler(address.p,handler.p);if(document.p)uia->RemovePropertyChangedEventHandler(document.p,handler.p);}
invalidateBrowser();
// COM objects release before uninitializing the apartment below.
if(header.p){header.p->Release();header.p=nullptr;}
if(address.p){address.p->Release();address.p=nullptr;}if(document.p){document.p->Release();document.p=nullptr;}
if(uia.p){uia.p->Release();uia.p=nullptr;}CoUninitialize();
});
}
void stopBrowserChatMonitor(){browserStopping=true;auto id=browserThreadId.load();if(id)PostThreadMessageW(id,WM_QUIT,0,0);if(browserThread.joinable())browserThread.join();browserPending=false;}
}
+26
View File
@@ -4,6 +4,31 @@
namespace ks namespace ks
{ {
inline std::wstring telegramWebKey(std::wstring url)
{
if (url.starts_with(L"https://")) url.erase(0, 8);
const std::wstring host = L"web.telegram.org/";
if (!url.starts_with(host)) return {};
url.erase(0, host.size());
if (!(url.starts_with(L"k/#") || url.starts_with(L"a/#"))) return {};
auto peer = url.substr(3);
if (peer.empty() || peer.size() > 128) return {};
if (peer.front() == L'@')
{
peer = lower(peer);
if (peer.size() < 2 || peer.find_first_not_of(L"abcdefghijklmnopqrstuvwxyz0123456789_", 1) != peer.npos) return {};
}
else
{
auto first = peer.front() == L'-' ? 1u : 0u;
if (first == peer.size() || peer.find_first_not_of(L"0123456789", first) != peer.npos) return {};
}
return L"telegram-web\n" + peer;
}
std::wstring readChromeChat(HWND);
std::wstring cachedChromeChat(HWND);
void startBrowserChatMonitor();
void stopBrowserChatMonitor();
struct ChatSwitchHook struct ChatSwitchHook
{ {
bool pressed[256]{}; bool pressed[256]{};
@@ -64,6 +89,7 @@ inline std::wstring chatKey(std::wstring process, std::wstring title)
} }
inline std::wstring windowChatKey(HWND window) inline std::wstring windowChatKey(HWND window)
{ {
if (window && lower(processName(window)) == L"chrome") return cachedChromeChat(window);
return window ? chatKey(processName(window), chatTitle(window)) : L""; return window ? chatKey(processName(window), chatTitle(window)) : L"";
} }
inline std::vector<std::wstring> chatCycle(const Settings &settings, const std::wstring &key, inline std::vector<std::wstring> chatCycle(const Settings &settings, const std::wstring &key,
+2
View File
@@ -733,3 +733,5 @@ void log(const std::wstring &message)
} }
} }
} // namespace ks } // namespace ks
#include "browser_chat.inc"
+1 -1
View File
@@ -67,7 +67,7 @@ void validateSettings(const Settings &s)
auto separator = key.find(L'\n'); auto separator = key.find(L'\n');
if (separator == key.npos || separator + 1 == key.size() || key.size() > 1024 || if (separator == key.npos || separator + 1 == key.size() || key.size() > 1024 ||
key.find(L'\n', separator + 1) != key.npos || key.find(L'\r') != key.npos || key.find(L'\n', separator + 1) != key.npos || key.find(L'\r') != key.npos ||
(key.substr(0, separator) != L"telegram" && key.substr(0, separator) != L"ms-teams" && key.substr(0, separator) != L"viber")) (key.substr(0, separator) != L"telegram" && key.substr(0, separator) != L"ms-teams" && key.substr(0, separator) != L"viber" && key.substr(0, separator) != L"telegram-web"))
throw std::runtime_error("Invalid chat language profile"); throw std::runtime_error("Invalid chat language profile");
if (cycle.empty() || cycle.size() > 64) throw std::runtime_error("Select at least one chat language"); if (cycle.empty() || cycle.size() > 64) throw std::runtime_error("Select at least one chat language");
std::vector<std::wstring> seen; std::vector<std::wstring> seen;
+5 -5
View File
@@ -1013,7 +1013,7 @@ LRESULT CALLBACK chatLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{ {
int selected = (int)SendDlgItemMessageW(w, 710, LB_GETCURSEL, 0, 0); int selected = (int)SendDlgItemMessageW(w, 710, LB_GETCURSEL, 0, 0);
if (selected < 0 || selected >= (int)v->keys.size()) if (selected < 0 || selected >= (int)v->keys.size())
throw std::runtime_error("Open a Telegram or Teams chat, then reopen this dialog. Viber OCR is disabled."); throw std::runtime_error("Open Telegram, Telegram Web K in Chrome, or Teams, then reopen this dialog. Viber OCR is disabled.");
auto next = v->settings->draft; auto next = v->settings->draft;
if (id == 713) next.chatCycles.erase(v->keys[selected]); if (id == 713) next.chatCycles.erase(v->keys[selected]);
else else
@@ -1055,7 +1055,7 @@ void showChatLanguages(HWND parent, SettingsView *settings)
{ {
if (!IsWindowVisible(candidate)) return TRUE; if (!IsWindowVisible(candidate)) return TRUE;
auto &data = *(Context *)context; auto &data = *(Context *)context;
auto key = windowChatKey(candidate); auto key = lower(processName(candidate)) == L"chrome" ? readChromeChat(candidate) : windowChatKey(candidate);
if (!key.empty()) data.keys.insert(std::move(key)); if (!key.empty()) data.keys.insert(std::move(key));
return TRUE; return TRUE;
}, (LPARAM)&context); }, (LPARAM)&context);
@@ -1063,13 +1063,13 @@ void showChatLanguages(HWND parent, SettingsView *settings)
auto *v = new ChatLanguagesView{parent, settings, {keys.begin(), keys.end()}}; auto *v = new ChatLanguagesView{parent, settings, {keys.begin(), keys.end()}};
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
label(w, L"Telegram / Teams profiles; saved Viber profiles are inactive", 18, 16, 604); label(w, L"Telegram / Telegram Web (Chrome) / Teams; Viber inactive", 18, 16, 604);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER,
710, 18, 44, 604, 140); 710, 18, 44, 604, 140);
for (const auto &key : v->keys) for (const auto &key : v->keys)
{ {
auto split = key.find(L'\n'); auto split = key.find(L'\n');
auto name = std::wstring(key.starts_with(L"telegram\n") ? L"Telegram: " : key.starts_with(L"viber\n") ? L"Viber (inactive): " : L"Teams: ") + key.substr(split + 1); auto name = std::wstring(key.starts_with(L"telegram\n") ? L"Telegram: " : key.starts_with(L"telegram-web\n") ? L"Telegram Web: " : key.starts_with(L"viber\n") ? L"Viber (inactive): " : L"Teams: ") + key.substr(split + 1);
SendDlgItemMessageW(w, 710, LB_ADDSTRING, 0, (LPARAM)name.c_str()); SendDlgItemMessageW(w, 710, LB_ADDSTRING, 0, (LPARAM)name.c_str());
} }
control(w, L"STATIC", L"Open a chat, then reopen this dialog to add it.", 0, 712, 18, 194, 604, 26); control(w, L"STATIC", L"Open a chat, then reopen this dialog to add it.", 0, 712, 18, 194, 604, 26);
@@ -1988,7 +1988,7 @@ void showSettings(App &app, bool visible)
label(w, L"Backspace / Delete clears it", 454, 270, 230); label(w, L"Backspace / Delete clears it", 454, 270, 230);
button(w, L"Re&fresh layouts", 253, 454, 322, 230); button(w, L"Re&fresh layouts", 253, 454, 322, 230);
button(w, L"Languages for a &chat...", 259, 24, 378, 230); button(w, L"Languages for a &chat...", 259, 24, 378, 230);
label(w, L"Chat languages: Telegram / Teams. Viber OCR disabled.", 272, 382, 412); label(w, L"Telegram / Telegram Web (Chrome) / Teams", 272, 382, 412);
page(); page();
label(w, L"Click a shortcut field and press the desired keys", 24, 76, 660); label(w, L"Click a shortcut field and press the desired keys", 24, 76, 660);
+8
View File
@@ -14,6 +14,13 @@ void require(bool ok, const char *message)
} }
void contextChecks() void contextChecks()
{ {
require(ks::telegramWebKey(L"https://web.telegram.org/k/#@Example") == L"telegram-web\n@example", "Telegram Web username normalized");
require(ks::telegramWebKey(L"web.telegram.org/a/#-123456") == L"telegram-web\n-123456", "Telegram Web numeric peer");
for (const auto *url : {L"https://web.telegram.org/k/", L"https://web.telegram.org.evil/k/#@example",
L"http://web.telegram.org/k/#@example", L"https://evil/web.telegram.org/k/#@example",
L"https://web.telegram.org/k/#@example?foo", L"https://web.telegram.org/k/#@",
L"https://web.telegram.org/k/#-", L"https://web.telegram.org/k/#settings"})
require(ks::telegramWebKey(url).empty(), "Reject non-chat or untrusted Telegram Web URL");
const auto key = ks::chatKey(L"Telegram.exe", L"\u200eТестовий чат \u2013 (12)"); const auto key = ks::chatKey(L"Telegram.exe", L"\u200eТестовий чат \u2013 (12)");
require(key == L"telegram\nТестовий чат", "Telegram unread suffix"); require(key == L"telegram\nТестовий чат", "Telegram unread suffix");
require(ks::chatKey(L"Telegram", L"Тестовий чат") == key, "stable unread profile"); require(ks::chatKey(L"Telegram", L"Тестовий чат") == key, "stable unread profile");
@@ -28,6 +35,7 @@ void contextChecks()
settings.cycle = {L"win:04090409", L"win:04220422", L"win:04190419"}; settings.cycle = {L"win:04090409", L"win:04220422", L"win:04190419"};
settings.chatCycles[key] = {settings.cycle[1], settings.cycle[0]}; settings.chatCycles[key] = {settings.cycle[1], settings.cycle[0]};
settings.chatCycles[L"viber\nТестовий контакт"] = {settings.cycle[1], settings.cycle[0]}; settings.chatCycles[L"viber\nТестовий контакт"] = {settings.cycle[1], settings.cycle[0]};
settings.chatCycles[L"telegram-web\n@example"] = {settings.cycle[0], settings.cycle[1]};
std::vector<ks::Layout> installed(3); std::vector<ks::Layout> installed(3);
for (size_t i = 0; i < installed.size(); ++i) for (size_t i = 0; i < installed.size(); ++i)
{ {