"use strict"; function byId(id, nullOk=false) { const el = document.getElementById(id); if (!el && !nullOk) { console.error(`No element #${id}`); } return el; } const LOCALSTORAGE_KEY_SERVER = "mattermostServer"; const RE_SERVER_ITEM = new RegExp(`^${LOCALSTORAGE_KEY_SERVER}_(.*)\$`, ""); const Storage = { getServers() { let servers = []; for (var i = 0; i < window.localStorage.length; i++) { const key = window.localStorage.key(i); const matches = key.match(RE_SERVER_ITEM); if (matches) { const endpoint = matches[1]; console.debug(`Found logged in endpoint ${endpoint}`); let stored = JSON.parse(window.localStorage.getItem(Storage._key_for(endpoint))); servers.push({...stored, endpoint}); } } return servers; }, _key_for(endpoint) { return `${LOCALSTORAGE_KEY_SERVER}_${endpoint}`; }, clear(endpoint) { window.localStorage.removeItem(Storage._key_for(endpoint)); }, store(endpoint, login_id, token) { window.localStorage.setItem(Storage._key_for(endpoint), JSON.stringify({login_id, token})); }, get(endpoint) { return JSON.parse(window.localStorage.getItem(Storage._key_for(endpoint)) || "null"); } } class MattermostApi { constructor(endpoint) { this._endpoint = endpoint; } get id() { return this._endpoint; } async get(path, token) { const headers = token ? {"Authorization": `Bearer ${token}`} : {}; const response = await ajax.getJson(`${this._endpoint}${path}`, {headers}); if (!response.ok) { throw response; } return response; } async post(path, data, token) { const headers = token ? {"Authorization": `Bearer ${token}`} : {}; const response = await ajax.postJson(`${this._endpoint}${path}`, data, {headers}); if (!response.ok) { throw response; } return response; } } class MattermostClient { constructor (api, storage) { this.api = api; this.storage = storage; } async logIn(login_id, password) { const response = await this.api.post("/users/login", {login_id, password}); const token = response.getResponseHeader("Token"); if (!token) { throw Error("No Token header in response to log in request"); } this.storage.store(this.api.id, login_id, token); return response.responseJson; } async logOut() { const stored = this.storage.get(this.api.id); if (!stored || !stored.token) { throw Error("No token stored"); } const response = await this.api.post("/users/logout", undefined, stored.token); // Verify that the token is now invalidated let tokenWorks; try { const meResponse = await this.usersMe(); tokenWorks = true; } catch (e) { if (e instanceof ajax.NotOkError && e.xhr.status == 401) { tokenWorks = false; } else { throw e; } } if (tokenWorks) { throw new Error("Failed to log out: token still works after trying to log out"); } this.storage.clear(this.api.id); return response.responseJson; } async usersMe() { const stored = this.storage.get(this.api.id); if (!stored || !stored.token) { throw Error("No token stored"); } const response = await this.api.get("/users/me", stored.token); return response.responseJson; } async myTeams() { const stored = this.storage.get(this.api.id); const response = await this.api.get("/users/me/teams", stored.token); return response.responseJson; } async myChannels(team_id) { const stored = this.storage.get(this.api.id); const response = await this.api.get(`/users/me/teams/${team_id}/channels`, stored.token); return response.responseJson; } } /** * Return an endpoint URL that has a protocol, domain and path */ function normalizedEndpoint(endpoint) { let matches = endpoint.match(/^(https?:\/\/)?([^\/]+)(\/.*)?$/i); if (!matches) throw Error("Invalid endpoint URL"); let protocol = matches[1] || "https://"; let domain = matches[2]; let path = matches[3] || "/api/v4"; return `${protocol}${domain}${path}`; } function humanReadableEndpoint(endpoint) { let matches = endpoint.match(/^(https?:\/\/.+)\/api\/v4$/i); if (!matches) throw Error("Invalid endpoint URL"); return matches[1]; } function createClient(endpoint) { const api = new MattermostApi(normalizedEndpoint(endpoint)); return new MattermostClient(api, Storage); } function buttonDisable(element, text) { if (!element.dataset.originalText) { element.dataset.originalText = element.innerText; } element.innerText = text; element.disabled = true; } function buttonEnable(element) { element.innerText = element.dataset.originalText; element.disabled = false; } function populateServerSelectionList() { const servers = Storage.getServers(); let nodes = []; for (let server of servers) { const li = document.createElement("li"); const endpoint = humanReadableEndpoint(server.endpoint); li.innerText = `${server.login_id}@${endpoint} `; const logoutButton = document.createElement("button"); logoutButton.className = "logout"; logoutButton.innerText = "Log out"; logoutButton.addEventListener("click", e => logOut(endpoint, e.currentTarget)); li.appendChild(logoutButton); nodes.push(li); } byId("server_selection_list").innerHTML = ""; byId("server_selection_list").append(...nodes); } populateServerSelectionList(); function populateChannelList() { async function addChannelItems(endpoint) { const client = createClient(endpoint); const teams = await client.myTeams(); console.log(teams); for (let team of teams) { let nodes = []; const channels = await client.myChannels(team.id); console.log(channels); for (let channel of channels) { const li = document.createElement("li"); const a = document.createElement("a"); a.href = "javascript:void(0)"; switch (channel.type) { case "O": // Public channel a.innerText = `${team.name}/${channel.name}`; break; case "P": // Private channel a.innerText = `🔒 ${team.name}/${channel.name}`; break; case "D": // Direct message a.innerText = `👤 ...`; break; case "G": // Group chat a.innerText = `👥 ${channel.display_name}`; break; default: // Unsupported a.innerText = `${channel.type} ${team.name}/${channel.name}`; break; } a.addEventListener("click", () => switchToChannel(team.id, channel.id)); li.appendChild(a); nodes.push(li); } byId("channel_list").append(...nodes); } } const servers = Storage.getServers(); byId("channel_list").innerHTML = ""; for (let server of servers) { addChannelItems(server.endpoint); } } populateChannelList(); function logIn() { const client = createClient(byId("login_server").value); buttonDisable(byId("login_button"), "Logging in..."); client.logIn(byId("login_login_id").value, byId("login_password").value) .then(json => { buttonEnable(byId("login_button")); byId("login_message").innerText = ""; byId("channel_list").innerText = `Logged in as ${json.username}`; window.location = "#"; populateServerSelectionList(); populateChannelList(); }) .catch(error => { console.error(error); buttonEnable(byId("login_button")); byId("login_message").innerText = `${error}`; }); } function logOut(endpoint, button) { const client = createClient(endpoint); buttonDisable(button, "Logging out..."); client.logOut() .then(response => { console.info("Succesfully logged out"); populateServerSelectionList(); populateChannelList(); }) .catch(error => { console.error(error); const span = document.createElement("span"); span.innerText = `Failed to log out: ${error.message}`; button.parentElement.appendChild(span); buttonEnable(button); }); } function switchToChannel(team_id, channel_id) { window.location = "#channel_contents"; } byId("login_button").addEventListener("click", logIn);