Просмотр исходного кода

Fixes #1181. Show OMEMO fingerprint as QR code

JC Brand 4 месяцев назад
Родитель
Сommit
bffb79dbad

+ 1 - 0
CHANGES.md

@@ -11,6 +11,7 @@
 - #1038: Support setting node config manually
 - #1057: Removed the `mobile` view mode. Instead of setting `view_mode` to `mobile`, set it to `fullscreen`.
 - #1174: Show MUC avatars in the rooms list
+- #1181: Show OMEMO fingerprint as QR code
 - #1195: Add actions to quote and copy messages
 - #1303: Display non-contacts who sent us a message somehow in fullscreen 
 - #1349: XEP-0392 Consistent Color Generation

+ 65 - 46
src/plugins/omemo/templates/profile.js

@@ -1,81 +1,100 @@
 import spinner from "templates/spinner.js";
-import { formatFingerprint } from 'plugins/omemo/utils.js';
+import { formatFingerprint, formatFingerprintForQRCode } from "plugins/omemo/utils.js";
 import { html } from "lit";
-import { __ } from 'i18n';
-
-
-const fingerprint = (el) => html`
-    <span class="fingerprint">${formatFingerprint(el.current_device.get('bundle').fingerprint)}</span>`;
+import { __ } from "i18n";
+import "shared/qrcode/component.js";
 
+/**
+ * @param {import('../profile').Profile} el
+ */
+function tplFingerprint(el) {
+    return html`<span class="fingerprint">${formatFingerprint(el.current_device.get("bundle").fingerprint)}</span>`;
+}
 
-const device_with_fingerprint = (el) => {
-    const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following fingerprint');
+function tplDeviceWithFingerprint(device) {
+    const i18n_fingerprint_checkbox_label = __("Checkbox for selecting the following fingerprint");
     return html`
         <li class="fingerprint-removal-item list-group-item">
             <label class="form-label">
-                <input type="checkbox" value="${el.device.get('id')}"
-                    aria-label="${i18n_fingerprint_checkbox_label}"/>
-                <span class="fingerprint">${formatFingerprint(el.device.get('bundle').fingerprint)}</span>
+                <input type="checkbox" value="${device.get("id")}" aria-label="${i18n_fingerprint_checkbox_label}" />
+                <span class="fingerprint">${formatFingerprint(device.get("bundle").fingerprint)}</span>
             </label>
         </li>
     `;
 }
 
-
-const device_without_fingerprint = (el) => {
-    const i18n_device_without_fingerprint = __('Device without a fingerprint');
-    const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following device');
+function tplDeviceWithoutFingerprint(device) {
+    const i18n_device_without_fingerprint = __("Device without a fingerprint");
+    const i18n_fingerprint_checkbox_label = __("Checkbox for selecting the following device");
     return html`
         <li class="fingerprint-removal-item list-group-item">
             <label class="form-label">
-            <input type="checkbox" value="${el.device.get('id')}"
-                aria-label="${i18n_fingerprint_checkbox_label}"/>
-            <span>${i18n_device_without_fingerprint}</span>
+                <input type="checkbox" value="${device.get("id")}" aria-label="${i18n_fingerprint_checkbox_label}" />
+                <span>${i18n_device_without_fingerprint}</span>
             </label>
         </li>
     `;
 }
 
+function tplDeviceItem(device) {
+    return html`${device.get("bundle") && device.get("bundle").fingerprint
+        ? tplDeviceWithFingerprint(device)
+        : tplDeviceWithoutFingerprint(device)}`;
+}
 
-const device_item = (el) => html`
-    ${(el.device.get('bundle') && el.device.get('bundle').fingerprint) ? device_with_fingerprint(el) : device_without_fingerprint(el) }
-`;
-
-
-const device_list = (el) => {
-    const i18n_other_devices = __('Other OMEMO-enabled devices');
-    const i18n_other_devices_label = __('Checkbox to select fingerprints of all other OMEMO devices');
-    const i18n_remove_devices = __('Remove checked devices and close');
-    const i18n_select_all = __('Select all');
+/**
+ * @param {import('../profile').Profile} el
+ */
+function tplDeviceList(el) {
+    const i18n_other_devices = __("Other OMEMO-enabled devices");
+    const i18n_other_devices_label = __("Checkbox to select fingerprints of all other OMEMO devices");
+    const i18n_remove_devices = __("Remove checked devices and close");
+    const i18n_select_all = __("Select all");
     return html`
         <ul class="list-group fingerprints">
             <li class="list-group-item active">
                 <label class="form-label">
-                    <input type="checkbox" class="select-all" @change=${el.selectAll} title="${i18n_select_all}" aria-label="${i18n_other_devices_label}"/>
+                    <input
+                        type="checkbox"
+                        class="select-all"
+                        @change=${el.selectAll}
+                        title="${i18n_select_all}"
+                        aria-label="${i18n_other_devices_label}"
+                    />
                     ${i18n_other_devices}
                 </label>
             </li>
-            ${ el.other_devices?.map(device => device_item(Object.assign({device}, el))) }
+            ${el.other_devices?.map((device) => tplDeviceItem(device))}
         </ul>
         <div><button type="submit" class="save-form btn btn-primary">${i18n_remove_devices}</button></div>
     `;
 }
 
-
+/**
+ * @param {import('../profile').Profile} el
+ */
 export default (el) => {
     const i18n_fingerprint = __("This device's OMEMO fingerprint");
-    const i18n_generate = __('Generate new keys and fingerprint');
-    return html`
-        <form class="converse-form fingerprint-removal" @submit=${el.removeSelectedFingerprints}>
-            <ul class="list-group fingerprints">
-                <li class="list-group-item active">${i18n_fingerprint}</li>
-                <li class="list-group-item">
-                    ${ (el.current_device && el.current_device.get('bundle') && el.current_device.get('bundle').fingerprint) ? fingerprint(el) : spinner() }
-                </li>
-            </ul>
-            <div class="pb-3">
-                <button type="button" class="generate-bundle btn btn-danger" @click=${el.generateOMEMODeviceBundle}>${i18n_generate}</button>
-            </div>
-            ${ el.other_devices?.length ? device_list(el) : '' }
-        </form>`;
-}
+    const i18n_generate = __("Generate new keys and fingerprint");
+    const fingerprint = el?.current_device?.get("bundle").fingerprint;
+    return html` <form class="converse-form fingerprint-removal" @submit=${el.removeSelectedFingerprints}>
+        <ul class="list-group fingerprints">
+            <li class="list-group-item active">${i18n_fingerprint}</li>
+            <li class="list-group-item">${fingerprint ? tplFingerprint(el) : spinner()}</li>
+            <li class="list-group-item p-4">
+                ${fingerprint
+                    ? html`<converse-qr-code
+                          class="centered"
+                          text="${formatFingerprintForQRCode(fingerprint)}"
+                      ></converse-qr-code>`
+                    : ""}
+            </li>
+        </ul>
+        <div class="pb-3">
+            <button type="button" class="generate-bundle btn btn-danger" @click=${el.generateOMEMODeviceBundle}>
+                ${i18n_generate}
+            </button>
+        </div>
+        ${el.other_devices?.length ? tplDeviceList(el) : ""}
+    </form>`;
+};

+ 7 - 0
src/plugins/omemo/utils.js

@@ -44,6 +44,13 @@ export function formatFingerprint (fp) {
     return fp;
 }
 
+export function formatFingerprintForQRCode (fp) {
+    const sid = _converse.state.omemo_store.get('device_id');
+    const jid = _converse.session.get('bare_jid');
+    fp = fp.replace(/^05/, '');
+    return `xmpp:${jid}?omemo-sid-${sid}=${fp}`;
+}
+
 /**
  * @param {Error|IQError|UserFacingError} e
  * @param {ChatBox} chat

+ 1 - 1
src/shared/components/image-picker.js

@@ -1,8 +1,8 @@
 import { html } from 'lit';
 import { Model } from '@converse/skeletor';
+import { api } from "@converse/headless";
 import { CustomElement } from './element.js';
 import { __ } from 'i18n';
-import { api } from "@converse/headless";
 
 const i18n_profile_picture = __('Click to set a new picture');
 

+ 10 - 0
src/shared/qrcode/README.md

@@ -0,0 +1,10 @@
+# converse-qr-code
+
+This package implements a web component `converse-qr-code` which renders a QR
+code representing provided text.
+
+How to use:
+
+```
+<converse-qr-code text="https://conversejs.org"></converse-qr-code>
+```

+ 69 - 0
src/shared/qrcode/component.js

@@ -0,0 +1,69 @@
+import { svg } from "lit";
+import { api } from "@converse/headless";
+import { CustomElement } from "shared/components/element.js";
+import { QRCodeModel } from "./generator.js";
+import { QRErrorCorrectLevelMap } from "./constants.js";
+
+import "./qrcode.scss";
+
+class QRCodeComponent extends CustomElement {
+    static get properties() {
+        return {
+            text: { type: String },
+            width: { type: String },
+            height: { type: String },
+        };
+    }
+
+    /** @type {QRCodeModel} */
+    #qr_code;
+
+    constructor() {
+        super();
+        this.#qr_code = null;
+        this.text = null;
+        this.width = '200px';
+        this.height = '200px';
+        this.colorDark = "#000000";
+        this.colorLight = "#ffffff";
+        this.correctLevel = QRErrorCorrectLevelMap.H;
+    }
+
+    render() {
+        const nCount = this.#qr_code.getModuleCount();
+        const rects = [];
+
+        for (let row = 0; row < nCount; row++) {
+            for (let col = 0; col < nCount; col++) {
+                if (this.#qr_code.isDark(row, col)) {
+                    rects.push(svg`<use x="${row}" y="${col}" href="#template"></use>`);
+                }
+            }
+        }
+
+        return svg`
+            <svg viewBox="0 0 ${nCount} ${nCount}" width="${this.width}" height="${this.height}" fill="${this.colorLight}">
+                <rect fill="${this.colorLight}" width="100%" height="100%"></rect>
+                <rect fill="${this.colorDark}" width="1" height="1" id="template"></rect>
+                ${rects}
+            </svg>
+        `;
+    }
+
+    /**
+     * @param {import("lit").PropertyValues} changedProperties
+     */
+    shouldUpdate(changedProperties) {
+        if (!this.#qr_code || changedProperties.has('text') || changedProperties.has('correctLevel')) {
+            this.title = this.text;
+            this.#qr_code = new QRCodeModel(this.text, this.correctLevel);
+            this.#qr_code.make();
+            return super.shouldUpdate(changedProperties) || true;
+        }
+        return super.shouldUpdate(changedProperties);
+    }
+}
+
+api.elements.define("converse-qr-code", QRCodeComponent);
+
+export default QRCodeComponent;

+ 263 - 0
src/shared/qrcode/constants.js

@@ -0,0 +1,263 @@
+export const QRErrorCorrectLevelMap = { L: 1, M: 0, Q: 3, H: 2 };
+
+export const QRMode = { MODE_NUMBER: 1 << 0, MODE_ALPHA_NUM: 1 << 1, MODE_8BIT_BYTE: 1 << 2, MODE_KANJI: 1 << 3 };
+
+export const QRMaskPattern = {
+    PATTERN000: 0,
+    PATTERN001: 1,
+    PATTERN010: 2,
+    PATTERN011: 3,
+    PATTERN100: 4,
+    PATTERN101: 5,
+    PATTERN110: 6,
+    PATTERN111: 7,
+};
+
+export const QRCodeLimitLength = [
+    [17, 14, 11, 7],
+    [32, 26, 20, 14],
+    [53, 42, 32, 24],
+    [78, 62, 46, 34],
+    [106, 84, 60, 44],
+    [134, 106, 74, 58],
+    [154, 122, 86, 64],
+    [192, 152, 108, 84],
+    [230, 180, 130, 98],
+    [271, 213, 151, 119],
+    [321, 251, 177, 137],
+    [367, 287, 203, 155],
+    [425, 331, 241, 177],
+    [458, 362, 258, 194],
+    [520, 412, 292, 220],
+    [586, 450, 322, 250],
+    [644, 504, 364, 280],
+    [718, 560, 394, 310],
+    [792, 624, 442, 338],
+    [858, 666, 482, 382],
+    [929, 711, 509, 403],
+    [1003, 779, 565, 439],
+    [1091, 857, 611, 461],
+    [1171, 911, 661, 511],
+    [1273, 997, 715, 535],
+    [1367, 1059, 751, 593],
+    [1465, 1125, 805, 625],
+    [1528, 1190, 868, 658],
+    [1628, 1264, 908, 698],
+    [1732, 1370, 982, 742],
+    [1840, 1452, 1030, 790],
+    [1952, 1538, 1112, 842],
+    [2068, 1628, 1168, 898],
+    [2188, 1722, 1228, 958],
+    [2303, 1809, 1283, 983],
+    [2431, 1911, 1351, 1051],
+    [2563, 1989, 1423, 1093],
+    [2699, 2099, 1499, 1139],
+    [2809, 2213, 1579, 1219],
+    [2953, 2331, 1663, 1273],
+];
+
+export const PATTERN_POSITION_TABLE = [
+    [],
+    [6, 18],
+    [6, 22],
+    [6, 26],
+    [6, 30],
+    [6, 34],
+    [6, 22, 38],
+    [6, 24, 42],
+    [6, 26, 46],
+    [6, 28, 50],
+    [6, 30, 54],
+    [6, 32, 58],
+    [6, 34, 62],
+    [6, 26, 46, 66],
+    [6, 26, 48, 70],
+    [6, 26, 50, 74],
+    [6, 30, 54, 78],
+    [6, 30, 56, 82],
+    [6, 30, 58, 86],
+    [6, 34, 62, 90],
+    [6, 28, 50, 72, 94],
+    [6, 26, 50, 74, 98],
+    [6, 30, 54, 78, 102],
+    [6, 28, 54, 80, 106],
+    [6, 32, 58, 84, 110],
+    [6, 30, 58, 86, 114],
+    [6, 34, 62, 90, 118],
+    [6, 26, 50, 74, 98, 122],
+    [6, 30, 54, 78, 102, 126],
+    [6, 26, 52, 78, 104, 130],
+    [6, 30, 56, 82, 108, 134],
+    [6, 34, 60, 86, 112, 138],
+    [6, 30, 58, 86, 114, 142],
+    [6, 34, 62, 90, 118, 146],
+    [6, 30, 54, 78, 102, 126, 150],
+    [6, 24, 50, 76, 102, 128, 154],
+    [6, 28, 54, 80, 106, 132, 158],
+    [6, 32, 58, 84, 110, 136, 162],
+    [6, 26, 54, 82, 110, 138, 166],
+    [6, 30, 58, 86, 114, 142, 170],
+];
+
+export const RS_BLOCK_TABLE = [
+    [1, 26, 19],
+    [1, 26, 16],
+    [1, 26, 13],
+    [1, 26, 9],
+    [1, 44, 34],
+    [1, 44, 28],
+    [1, 44, 22],
+    [1, 44, 16],
+    [1, 70, 55],
+    [1, 70, 44],
+    [2, 35, 17],
+    [2, 35, 13],
+    [1, 100, 80],
+    [2, 50, 32],
+    [2, 50, 24],
+    [4, 25, 9],
+    [1, 134, 108],
+    [2, 67, 43],
+    [2, 33, 15, 2, 34, 16],
+    [2, 33, 11, 2, 34, 12],
+    [2, 86, 68],
+    [4, 43, 27],
+    [4, 43, 19],
+    [4, 43, 15],
+    [2, 98, 78],
+    [4, 49, 31],
+    [2, 32, 14, 4, 33, 15],
+    [4, 39, 13, 1, 40, 14],
+    [2, 121, 97],
+    [2, 60, 38, 2, 61, 39],
+    [4, 40, 18, 2, 41, 19],
+    [4, 40, 14, 2, 41, 15],
+    [2, 146, 116],
+    [3, 58, 36, 2, 59, 37],
+    [4, 36, 16, 4, 37, 17],
+    [4, 36, 12, 4, 37, 13],
+    [2, 86, 68, 2, 87, 69],
+    [4, 69, 43, 1, 70, 44],
+    [6, 43, 19, 2, 44, 20],
+    [6, 43, 15, 2, 44, 16],
+    [4, 101, 81],
+    [1, 80, 50, 4, 81, 51],
+    [4, 50, 22, 4, 51, 23],
+    [3, 36, 12, 8, 37, 13],
+    [2, 116, 92, 2, 117, 93],
+    [6, 58, 36, 2, 59, 37],
+    [4, 46, 20, 6, 47, 21],
+    [7, 42, 14, 4, 43, 15],
+    [4, 133, 107],
+    [8, 59, 37, 1, 60, 38],
+    [8, 44, 20, 4, 45, 21],
+    [12, 33, 11, 4, 34, 12],
+    [3, 145, 115, 1, 146, 116],
+    [4, 64, 40, 5, 65, 41],
+    [11, 36, 16, 5, 37, 17],
+    [11, 36, 12, 5, 37, 13],
+    [5, 109, 87, 1, 110, 88],
+    [5, 65, 41, 5, 66, 42],
+    [5, 54, 24, 7, 55, 25],
+    [11, 36, 12],
+    [5, 122, 98, 1, 123, 99],
+    [7, 73, 45, 3, 74, 46],
+    [15, 43, 19, 2, 44, 20],
+    [3, 45, 15, 13, 46, 16],
+    [1, 135, 107, 5, 136, 108],
+    [10, 74, 46, 1, 75, 47],
+    [1, 50, 22, 15, 51, 23],
+    [2, 42, 14, 17, 43, 15],
+    [5, 150, 120, 1, 151, 121],
+    [9, 69, 43, 4, 70, 44],
+    [17, 50, 22, 1, 51, 23],
+    [2, 42, 14, 19, 43, 15],
+    [3, 141, 113, 4, 142, 114],
+    [3, 70, 44, 11, 71, 45],
+    [17, 47, 21, 4, 48, 22],
+    [9, 39, 13, 16, 40, 14],
+    [3, 135, 107, 5, 136, 108],
+    [3, 67, 41, 13, 68, 42],
+    [15, 54, 24, 5, 55, 25],
+    [15, 43, 15, 10, 44, 16],
+    [4, 144, 116, 4, 145, 117],
+    [17, 68, 42],
+    [17, 50, 22, 6, 51, 23],
+    [19, 46, 16, 6, 47, 17],
+    [2, 139, 111, 7, 140, 112],
+    [17, 74, 46],
+    [7, 54, 24, 16, 55, 25],
+    [34, 37, 13],
+    [4, 151, 121, 5, 152, 122],
+    [4, 75, 47, 14, 76, 48],
+    [11, 54, 24, 14, 55, 25],
+    [16, 45, 15, 14, 46, 16],
+    [6, 147, 117, 4, 148, 118],
+    [6, 73, 45, 14, 74, 46],
+    [11, 54, 24, 16, 55, 25],
+    [30, 46, 16, 2, 47, 17],
+    [8, 132, 106, 4, 133, 107],
+    [8, 75, 47, 13, 76, 48],
+    [7, 54, 24, 22, 55, 25],
+    [22, 45, 15, 13, 46, 16],
+    [10, 142, 114, 2, 143, 115],
+    [19, 74, 46, 4, 75, 47],
+    [28, 50, 22, 6, 51, 23],
+    [33, 46, 16, 4, 47, 17],
+    [8, 152, 122, 4, 153, 123],
+    [22, 73, 45, 3, 74, 46],
+    [8, 53, 23, 26, 54, 24],
+    [12, 45, 15, 28, 46, 16],
+    [3, 147, 117, 10, 148, 118],
+    [3, 73, 45, 23, 74, 46],
+    [4, 54, 24, 31, 55, 25],
+    [11, 45, 15, 31, 46, 16],
+    [7, 146, 116, 7, 147, 117],
+    [21, 73, 45, 7, 74, 46],
+    [1, 53, 23, 37, 54, 24],
+    [19, 45, 15, 26, 46, 16],
+    [5, 145, 115, 10, 146, 116],
+    [19, 75, 47, 10, 76, 48],
+    [15, 54, 24, 25, 55, 25],
+    [23, 45, 15, 25, 46, 16],
+    [13, 145, 115, 3, 146, 116],
+    [2, 74, 46, 29, 75, 47],
+    [42, 54, 24, 1, 55, 25],
+    [23, 45, 15, 28, 46, 16],
+    [17, 145, 115],
+    [10, 74, 46, 23, 75, 47],
+    [10, 54, 24, 35, 55, 25],
+    [19, 45, 15, 35, 46, 16],
+    [17, 145, 115, 1, 146, 116],
+    [14, 74, 46, 21, 75, 47],
+    [29, 54, 24, 19, 55, 25],
+    [11, 45, 15, 46, 46, 16],
+    [13, 145, 115, 6, 146, 116],
+    [14, 74, 46, 23, 75, 47],
+    [44, 54, 24, 7, 55, 25],
+    [59, 46, 16, 1, 47, 17],
+    [12, 151, 121, 7, 152, 122],
+    [12, 75, 47, 26, 76, 48],
+    [39, 54, 24, 14, 55, 25],
+    [22, 45, 15, 41, 46, 16],
+    [6, 151, 121, 14, 152, 122],
+    [6, 75, 47, 34, 76, 48],
+    [46, 54, 24, 10, 55, 25],
+    [2, 45, 15, 64, 46, 16],
+    [17, 152, 122, 4, 153, 123],
+    [29, 74, 46, 14, 75, 47],
+    [49, 54, 24, 10, 55, 25],
+    [24, 45, 15, 46, 46, 16],
+    [4, 152, 122, 18, 153, 123],
+    [13, 74, 46, 32, 75, 47],
+    [48, 54, 24, 14, 55, 25],
+    [42, 45, 15, 32, 46, 16],
+    [20, 147, 117, 4, 148, 118],
+    [40, 75, 47, 7, 76, 48],
+    [43, 54, 24, 22, 55, 25],
+    [10, 45, 15, 67, 46, 16],
+    [19, 148, 118, 6, 149, 119],
+    [18, 75, 47, 31, 76, 48],
+    [34, 54, 24, 34, 55, 25],
+    [20, 45, 15, 61, 46, 16],
+];

+ 459 - 0
src/shared/qrcode/generator.js

@@ -0,0 +1,459 @@
+/**
+ * @copyright Kazuhiko Arase
+ * @copyright davidshimjs
+ * @license MIT
+ * The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
+ */
+import { QRErrorCorrectLevelMap, QRMode, RS_BLOCK_TABLE } from "./constants";
+import QRPolynomial from "./polynomial";
+import { QRUtil, getTypeNumber } from "./utils";
+
+export class QRCodeModel {
+    /**
+     * @param {String} text
+     * @param {import('./types').ErrorCorrectLevel} errorCorrectLevel
+     */
+    constructor(text, errorCorrectLevel) {
+        this.text = text;
+        this.errorCorrectLevel = errorCorrectLevel;
+        this.typeNumber = getTypeNumber(this.text, this.errorCorrectLevel);
+        this.modules = null;
+        this.moduleCount = 0;
+        this.dataCache = null;
+        this.dataList = [new QR8bitByte(this.text)];
+    }
+
+    static get PAD0() {
+        return 0xec;
+    }
+
+    static get PAD1() {
+        return 0x11;
+    }
+
+    /**
+     * @param {number} row
+     * @param {number} col
+     */
+    isDark(row, col) {
+        if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
+            throw new Error(row + "," + col);
+        }
+        return this.modules[row][col];
+    }
+
+    getModuleCount() {
+        return this.moduleCount;
+    }
+
+    make() {
+        this.makeImpl(false, this.getBestMaskPattern());
+    }
+
+    /**
+     * @param {boolean} test
+     * @param {Number} maskPattern
+     */
+    makeImpl(test, maskPattern) {
+        this.moduleCount = this.typeNumber * 4 + 17;
+        this.modules = new Array(this.moduleCount);
+        for (let row = 0; row < this.moduleCount; row++) {
+            this.modules[row] = new Array(this.moduleCount);
+            for (let col = 0; col < this.moduleCount; col++) {
+                this.modules[row][col] = null;
+            }
+        }
+        this.setupPositionProbePattern(0, 0);
+        this.setupPositionProbePattern(this.moduleCount - 7, 0);
+        this.setupPositionProbePattern(0, this.moduleCount - 7);
+        this.setupPositionAdjustPattern();
+        this.setupTimingPattern();
+        this.setupTypeInfo(test, maskPattern);
+        if (this.typeNumber >= 7) {
+            this.setupTypeNumber(test);
+        }
+        if (this.dataCache == null) {
+            this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
+        }
+        this.mapData(this.dataCache, maskPattern);
+    }
+
+    /**
+     * @param {number} row
+     * @param {number} col
+     */
+    setupPositionProbePattern(row, col) {
+        for (let r = -1; r <= 7; r++) {
+            if (row + r <= -1 || this.moduleCount <= row + r) continue;
+            for (let c = -1; c <= 7; c++) {
+                if (col + c <= -1 || this.moduleCount <= col + c) continue;
+                if (
+                    (0 <= r && r <= 6 && (c == 0 || c == 6)) ||
+                    (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
+                    (2 <= r && r <= 4 && 2 <= c && c <= 4)
+                ) {
+                    this.modules[row + r][col + c] = true;
+                } else {
+                    this.modules[row + r][col + c] = false;
+                }
+            }
+        }
+    }
+
+    /**
+     * @returns {Number}
+     */
+    getBestMaskPattern() {
+        let minLostPoint = 0;
+        let pattern = 0;
+        for (let i = 0; i < 8; i++) {
+            this.makeImpl(true, i);
+            let lostPoint = QRUtil.getLostPoint(this);
+            if (i == 0 || minLostPoint > lostPoint) {
+                minLostPoint = lostPoint;
+                pattern = i;
+            }
+        }
+        return pattern;
+    }
+
+    setupTimingPattern() {
+        for (let r = 8; r < this.moduleCount - 8; r++) {
+            if (this.modules[r][6] != null) {
+                continue;
+            }
+            this.modules[r][6] = r % 2 == 0;
+        }
+        for (let c = 8; c < this.moduleCount - 8; c++) {
+            if (this.modules[6][c] != null) {
+                continue;
+            }
+            this.modules[6][c] = c % 2 == 0;
+        }
+    }
+
+    setupPositionAdjustPattern() {
+        let pos = QRUtil.getPatternPosition(this.typeNumber);
+        for (let i = 0; i < pos.length; i++) {
+            for (let j = 0; j < pos.length; j++) {
+                let row = pos[i];
+                let col = pos[j];
+                if (this.modules[row][col] != null) {
+                    continue;
+                }
+                for (let r = -2; r <= 2; r++) {
+                    for (let c = -2; c <= 2; c++) {
+                        if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
+                            this.modules[row + r][col + c] = true;
+                        } else {
+                            this.modules[row + r][col + c] = false;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @param {boolean} test
+     */
+    setupTypeNumber(test) {
+        let bits = QRUtil.getBCHTypeNumber(this.typeNumber);
+        for (let i = 0; i < 18; i++) {
+            let mod = !test && ((bits >> i) & 1) == 1;
+            this.modules[Math.floor(i / 3)][(i % 3) + this.moduleCount - 8 - 3] = mod;
+        }
+        for (let i = 0; i < 18; i++) {
+            let mod = !test && ((bits >> i) & 1) == 1;
+            this.modules[(i % 3) + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+        }
+    }
+
+    /**
+     * @param {boolean} test
+     * @param {Number} maskPattern
+     */
+    setupTypeInfo(test, maskPattern) {
+        const data = (this.errorCorrectLevel << 3) | maskPattern;
+        const bits = QRUtil.getBCHTypeInfo(data);
+        for (let i = 0; i < 15; i++) {
+            let mod = !test && ((bits >> i) & 1) == 1;
+            if (i < 6) {
+                this.modules[i][8] = mod;
+            } else if (i < 8) {
+                this.modules[i + 1][8] = mod;
+            } else {
+                this.modules[this.moduleCount - 15 + i][8] = mod;
+            }
+        }
+        for (let i = 0; i < 15; i++) {
+            let mod = !test && ((bits >> i) & 1) == 1;
+            if (i < 8) {
+                this.modules[8][this.moduleCount - i - 1] = mod;
+            } else if (i < 9) {
+                this.modules[8][15 - i - 1 + 1] = mod;
+            } else {
+                this.modules[8][15 - i - 1] = mod;
+            }
+        }
+        this.modules[this.moduleCount - 8][8] = !test;
+    }
+
+    mapData(data, maskPattern) {
+        let inc = -1;
+        let row = this.moduleCount - 1;
+        let bitIndex = 7;
+        let byteIndex = 0;
+        for (let col = this.moduleCount - 1; col > 0; col -= 2) {
+            if (col == 6) col--;
+            while (true) {
+                for (let c = 0; c < 2; c++) {
+                    if (this.modules[row][col - c] == null) {
+                        let dark = false;
+                        if (byteIndex < data.length) {
+                            dark = ((data[byteIndex] >>> bitIndex) & 1) == 1;
+                        }
+                        let mask = QRUtil.getMask(maskPattern, row, col - c);
+                        if (mask) {
+                            dark = !dark;
+                        }
+                        this.modules[row][col - c] = dark;
+                        bitIndex--;
+                        if (bitIndex == -1) {
+                            byteIndex++;
+                            bitIndex = 7;
+                        }
+                    }
+                }
+                row += inc;
+                if (row < 0 || this.moduleCount <= row) {
+                    row -= inc;
+                    inc = -inc;
+                    break;
+                }
+            }
+        }
+    }
+
+    static createData(typeNumber, errorCorrectLevel, dataList) {
+        let rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
+        const buffer = new QRBitBuffer();
+        for (let i = 0; i < dataList.length; i++) {
+            let data = dataList[i];
+            buffer.put(data.mode, 4);
+            buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
+            data.write(buffer);
+        }
+        let totalDataCount = 0;
+        for (let i = 0; i < rsBlocks.length; i++) {
+            totalDataCount += rsBlocks[i].dataCount;
+        }
+        if (buffer.getLengthInBits() > totalDataCount * 8) {
+            throw new Error("code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")");
+        }
+        if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+            buffer.put(0, 4);
+        }
+        while (buffer.getLengthInBits() % 8 != 0) {
+            buffer.putBit(false);
+        }
+        while (true) {
+            if (buffer.getLengthInBits() >= totalDataCount * 8) {
+                break;
+            }
+            buffer.put(QRCodeModel.PAD0, 8);
+            if (buffer.getLengthInBits() >= totalDataCount * 8) {
+                break;
+            }
+            buffer.put(QRCodeModel.PAD1, 8);
+        }
+        return QRCodeModel.createBytes(buffer, rsBlocks);
+    }
+
+    static createBytes(buffer, rsBlocks) {
+        let offset = 0;
+        let maxDcCount = 0;
+        let maxEcCount = 0;
+        let dcdata = new Array(rsBlocks.length);
+        let ecdata = new Array(rsBlocks.length);
+        for (let r = 0; r < rsBlocks.length; r++) {
+            let dcCount = rsBlocks[r].dataCount;
+            let ecCount = rsBlocks[r].totalCount - dcCount;
+            maxDcCount = Math.max(maxDcCount, dcCount);
+            maxEcCount = Math.max(maxEcCount, ecCount);
+            dcdata[r] = new Array(dcCount);
+            for (let i = 0; i < dcdata[r].length; i++) {
+                dcdata[r][i] = 0xff & buffer.buffer[i + offset];
+            }
+            offset += dcCount;
+            let rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+            let rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
+            let modPoly = rawPoly.mod(rsPoly);
+            ecdata[r] = new Array(rsPoly.getLength() - 1);
+            for (let i = 0; i < ecdata[r].length; i++) {
+                let modIndex = i + modPoly.getLength() - ecdata[r].length;
+                ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
+            }
+        }
+        let totalCodeCount = 0;
+        for (let i = 0; i < rsBlocks.length; i++) {
+            totalCodeCount += rsBlocks[i].totalCount;
+        }
+        let data = new Array(totalCodeCount);
+        let index = 0;
+        for (let i = 0; i < maxDcCount; i++) {
+            for (let r = 0; r < rsBlocks.length; r++) {
+                if (i < dcdata[r].length) {
+                    data[index++] = dcdata[r][i];
+                }
+            }
+        }
+        for (let i = 0; i < maxEcCount; i++) {
+            for (let r = 0; r < rsBlocks.length; r++) {
+                if (i < ecdata[r].length) {
+                    data[index++] = ecdata[r][i];
+                }
+            }
+        }
+        return data;
+    }
+}
+
+class QR8bitByte {
+    /**
+     * @param {string} data
+     */
+    constructor(data) {
+        this.mode = QRMode.MODE_8BIT_BYTE;
+        this.data = data;
+        this.parsedData = [];
+
+        // Added to support UTF-8 Characters
+        for (let i = 0, l = this.data.length; i < l; i++) {
+            let byteArray = [];
+            let code = this.data.charCodeAt(i);
+
+            if (code > 0x10000) {
+                byteArray[0] = 0xf0 | ((code & 0x1c0000) >>> 18);
+                byteArray[1] = 0x80 | ((code & 0x3f000) >>> 12);
+                byteArray[2] = 0x80 | ((code & 0xfc0) >>> 6);
+                byteArray[3] = 0x80 | (code & 0x3f);
+            } else if (code > 0x800) {
+                byteArray[0] = 0xe0 | ((code & 0xf000) >>> 12);
+                byteArray[1] = 0x80 | ((code & 0xfc0) >>> 6);
+                byteArray[2] = 0x80 | (code & 0x3f);
+            } else if (code > 0x80) {
+                byteArray[0] = 0xc0 | ((code & 0x7c0) >>> 6);
+                byteArray[1] = 0x80 | (code & 0x3f);
+            } else {
+                byteArray[0] = code;
+            }
+
+            this.parsedData.push(byteArray);
+        }
+
+        this.parsedData = Array.prototype.concat.apply([], this.parsedData);
+
+        if (this.parsedData.length != this.data.length) {
+            this.parsedData.unshift(191);
+            this.parsedData.unshift(187);
+            this.parsedData.unshift(239);
+        }
+    }
+
+    getLength() {
+        return this.parsedData.length;
+    }
+
+    /**
+     * @param {QRBitBuffer} buffer
+     */
+    write(buffer) {
+        for (let i = 0, l = this.parsedData.length; i < l; i++) {
+            buffer.put(this.parsedData[i], 8);
+        }
+    }
+}
+
+class QRRSBlock {
+    constructor(totalCount, dataCount) {
+        this.totalCount = totalCount;
+        this.dataCount = dataCount;
+    }
+
+    static getRSBlocks(typeNumber, errorCorrectLevel) {
+        let rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
+        if (rsBlock == undefined) {
+            throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel);
+        }
+        let length = rsBlock.length / 3;
+        let list = [];
+        for (let i = 0; i < length; i++) {
+            let count = rsBlock[i * 3 + 0];
+            let totalCount = rsBlock[i * 3 + 1];
+            let dataCount = rsBlock[i * 3 + 2];
+            for (let j = 0; j < count; j++) {
+                list.push(new QRRSBlock(totalCount, dataCount));
+            }
+        }
+        return list;
+    }
+
+    static getRsBlockTable(typeNumber, errorCorrectLevel) {
+        switch (errorCorrectLevel) {
+            case QRErrorCorrectLevelMap.L:
+                return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+            case QRErrorCorrectLevelMap.M:
+                return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+            case QRErrorCorrectLevelMap.Q:
+                return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+            case QRErrorCorrectLevelMap.H:
+                return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+            default:
+                return undefined;
+        }
+    }
+}
+
+class QRBitBuffer {
+    constructor() {
+        this.buffer = [];
+        this.length = 0;
+    }
+
+    /**
+     * @param {Number} index
+     */
+    get(index) {
+        const bufIndex = Math.floor(index / 8);
+        return ((this.buffer[bufIndex] >>> (7 - (index % 8))) & 1) == 1;
+    }
+
+    /**
+     * @param {Number} num
+     * @param {Number} length
+     */
+    put(num, length) {
+        for (let i = 0; i < length; i++) {
+            this.putBit(((num >>> (length - i - 1)) & 1) == 1);
+        }
+    }
+
+    getLengthInBits() {
+        return this.length;
+    }
+
+    /**
+     * @param {Boolean} bit
+     */
+    putBit(bit) {
+        let bufIndex = Math.floor(this.length / 8);
+        if (this.buffer.length <= bufIndex) {
+            this.buffer.push(0);
+        }
+        if (bit) {
+            this.buffer[bufIndex] |= 0x80 >>> this.length % 8;
+        }
+        this.length++;
+    }
+}

+ 84 - 0
src/shared/qrcode/polynomial.js

@@ -0,0 +1,84 @@
+class QRPolynomial {
+    constructor(num, shift) {
+        if (num.length == undefined) {
+            throw new Error(num.length + "/" + shift);
+        }
+        let offset = 0;
+        while (offset < num.length && num[offset] == 0) {
+            offset++;
+        }
+        this.num = new Array(num.length - offset + shift);
+        for (let i = 0; i < num.length - offset; i++) {
+            this.num[i] = num[i + offset];
+        }
+    }
+
+    get(index) {
+        return this.num[index];
+    }
+
+    getLength() {
+        return this.num.length;
+    }
+
+    multiply(e) {
+        let num = new Array(this.getLength() + e.getLength() - 1);
+        for (let i = 0; i < this.getLength(); i++) {
+            for (let j = 0; j < e.getLength(); j++) {
+                num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
+            }
+        }
+        return new QRPolynomial(num, 0);
+    }
+
+    mod(e) {
+        if (this.getLength() - e.getLength() < 0) {
+            return this;
+        }
+        let ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
+        let num = new Array(this.getLength());
+        for (let i = 0; i < this.getLength(); i++) {
+            num[i] = this.get(i);
+        }
+        for (let i = 0; i < e.getLength(); i++) {
+            num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
+        }
+        return new QRPolynomial(num, 0).mod(e);
+    }
+}
+
+export default QRPolynomial;
+
+
+export const QRMath = {
+    glog: function (n) {
+        if (n < 1) {
+            throw new Error("glog(" + n + ")");
+        }
+        return QRMath.LOG_TABLE[n];
+    },
+    gexp: function (n) {
+        while (n < 0) {
+            n += 255;
+        }
+        while (n >= 256) {
+            n -= 255;
+        }
+        return QRMath.EXP_TABLE[n];
+    },
+    EXP_TABLE: new Array(256),
+    LOG_TABLE: new Array(256),
+};
+
+for (let i = 0; i < 8; i++) {
+    QRMath.EXP_TABLE[i] = 1 << i;
+}
+
+for (let i = 8; i < 256; i++) {
+    QRMath.EXP_TABLE[i] =
+        QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
+}
+
+for (let i = 0; i < 255; i++) {
+    QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
+}

+ 3 - 0
src/shared/qrcode/qrcode.scss

@@ -0,0 +1,3 @@
+converse-qr-code {
+    display: block;
+}

+ 12 - 0
src/shared/qrcode/types.ts

@@ -0,0 +1,12 @@
+import { QRErrorCorrectLevelMap } from './constants';
+
+export type ErrorCorrectLevel = typeof QRErrorCorrectLevelMap[keyof typeof QRErrorCorrectLevelMap];
+
+export type VOption = {
+     text: string; // QRCode link data
+     width?: number; // Default value is 256
+     height?: number; // Default value is 256
+     colorDark?: string; // Default "#000000"
+     colorLight?: string; // Default "#ffffff"
+     correctLevel?: ErrorCorrectLevel;
+}

+ 245 - 0
src/shared/qrcode/utils.js

@@ -0,0 +1,245 @@
+import { PATTERN_POSITION_TABLE, QRCodeLimitLength, QRErrorCorrectLevelMap, QRMaskPattern, QRMode } from "./constants";
+import QRPolynomial, { QRMath } from "./polynomial";
+
+/**
+ * Get the type by string length
+ * @param {String} text
+ * @param {Number} nCorrectLevel
+ * @return {Number} type
+ */
+export function getTypeNumber(text, nCorrectLevel) {
+    let nType = 1;
+    let length = getUTF8Length(text);
+
+    for (let i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
+        let nLimit = 0;
+
+        switch (nCorrectLevel) {
+            case QRErrorCorrectLevelMap.L:
+                nLimit = QRCodeLimitLength[i][0];
+                break;
+            case QRErrorCorrectLevelMap.M:
+                nLimit = QRCodeLimitLength[i][1];
+                break;
+            case QRErrorCorrectLevelMap.Q:
+                nLimit = QRCodeLimitLength[i][2];
+                break;
+            case QRErrorCorrectLevelMap.H:
+                nLimit = QRCodeLimitLength[i][3];
+                break;
+        }
+
+        if (length <= nLimit) {
+            break;
+        } else {
+            nType++;
+        }
+    }
+
+    if (nType > QRCodeLimitLength.length) {
+        throw new Error("Too long data");
+    }
+
+    return nType;
+}
+
+function getUTF8Length(sText) {
+    let replacedText = encodeURI(sText)
+        .toString()
+        .replace(/\%[0-9a-fA-F]{2}/g, "a");
+    return replacedText.length + (replacedText.length != sText ? 3 : 0);
+}
+
+export const QRUtil = {
+    G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0),
+    G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0),
+    G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),
+
+    getBCHTypeInfo(data) {
+        let d = data << 10;
+        while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
+            d ^= QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15));
+        }
+        return ((data << 10) | d) ^ QRUtil.G15_MASK;
+    },
+
+    getBCHTypeNumber(data) {
+        let d = data << 12;
+        while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
+            d ^= QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18));
+        }
+        return (data << 12) | d;
+    },
+
+    getBCHDigit(data) {
+        let digit = 0;
+        while (data != 0) {
+            digit++;
+            data >>>= 1;
+        }
+        return digit;
+    },
+
+    getPatternPosition(typeNumber) {
+        return PATTERN_POSITION_TABLE[typeNumber - 1];
+    },
+
+    getMask(maskPattern, i, j) {
+        switch (maskPattern) {
+            case QRMaskPattern.PATTERN000:
+                return (i + j) % 2 == 0;
+            case QRMaskPattern.PATTERN001:
+                return i % 2 == 0;
+            case QRMaskPattern.PATTERN010:
+                return j % 3 == 0;
+            case QRMaskPattern.PATTERN011:
+                return (i + j) % 3 == 0;
+            case QRMaskPattern.PATTERN100:
+                return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
+            case QRMaskPattern.PATTERN101:
+                return ((i * j) % 2) + ((i * j) % 3) == 0;
+            case QRMaskPattern.PATTERN110:
+                return (((i * j) % 2) + ((i * j) % 3)) % 2 == 0;
+            case QRMaskPattern.PATTERN111:
+                return (((i * j) % 3) + ((i + j) % 2)) % 2 == 0;
+            default:
+                throw new Error("bad maskPattern:" + maskPattern);
+        }
+    },
+
+    getErrorCorrectPolynomial(errorCorrectLength) {
+        let a = new QRPolynomial([1], 0);
+        for (let i = 0; i < errorCorrectLength; i++) {
+            a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
+        }
+        return a;
+    },
+
+    getLengthInBits(mode, type) {
+        if (1 <= type && type < 10) {
+            switch (mode) {
+                case QRMode.MODE_NUMBER:
+                    return 10;
+                case QRMode.MODE_ALPHA_NUM:
+                    return 9;
+                case QRMode.MODE_8BIT_BYTE:
+                    return 8;
+                case QRMode.MODE_KANJI:
+                    return 8;
+                default:
+                    throw new Error("mode:" + mode);
+            }
+        } else if (type < 27) {
+            switch (mode) {
+                case QRMode.MODE_NUMBER:
+                    return 12;
+                case QRMode.MODE_ALPHA_NUM:
+                    return 11;
+                case QRMode.MODE_8BIT_BYTE:
+                    return 16;
+                case QRMode.MODE_KANJI:
+                    return 10;
+                default:
+                    throw new Error("mode:" + mode);
+            }
+        } else if (type < 41) {
+            switch (mode) {
+                case QRMode.MODE_NUMBER:
+                    return 14;
+                case QRMode.MODE_ALPHA_NUM:
+                    return 13;
+                case QRMode.MODE_8BIT_BYTE:
+                    return 16;
+                case QRMode.MODE_KANJI:
+                    return 12;
+                default:
+                    throw new Error("mode:" + mode);
+            }
+        } else {
+            throw new Error("type:" + type);
+        }
+    },
+
+    getLostPoint(qrCode) {
+        let moduleCount = qrCode.getModuleCount();
+        let lostPoint = 0;
+        for (let row = 0; row < moduleCount; row++) {
+            for (let col = 0; col < moduleCount; col++) {
+                let sameCount = 0;
+                let dark = qrCode.isDark(row, col);
+                for (let r = -1; r <= 1; r++) {
+                    if (row + r < 0 || moduleCount <= row + r) {
+                        continue;
+                    }
+                    for (let c = -1; c <= 1; c++) {
+                        if (col + c < 0 || moduleCount <= col + c) {
+                            continue;
+                        }
+                        if (r == 0 && c == 0) {
+                            continue;
+                        }
+                        if (dark == qrCode.isDark(row + r, col + c)) {
+                            sameCount++;
+                        }
+                    }
+                }
+                if (sameCount > 5) {
+                    lostPoint += 3 + sameCount - 5;
+                }
+            }
+        }
+        for (let row = 0; row < moduleCount - 1; row++) {
+            for (let col = 0; col < moduleCount - 1; col++) {
+                let count = 0;
+                if (qrCode.isDark(row, col)) count++;
+                if (qrCode.isDark(row + 1, col)) count++;
+                if (qrCode.isDark(row, col + 1)) count++;
+                if (qrCode.isDark(row + 1, col + 1)) count++;
+                if (count == 0 || count == 4) {
+                    lostPoint += 3;
+                }
+            }
+        }
+        for (let row = 0; row < moduleCount; row++) {
+            for (let col = 0; col < moduleCount - 6; col++) {
+                if (
+                    qrCode.isDark(row, col) &&
+                    !qrCode.isDark(row, col + 1) &&
+                    qrCode.isDark(row, col + 2) &&
+                    qrCode.isDark(row, col + 3) &&
+                    qrCode.isDark(row, col + 4) &&
+                    !qrCode.isDark(row, col + 5) &&
+                    qrCode.isDark(row, col + 6)
+                ) {
+                    lostPoint += 40;
+                }
+            }
+        }
+        for (let col = 0; col < moduleCount; col++) {
+            for (let row = 0; row < moduleCount - 6; row++) {
+                if (
+                    qrCode.isDark(row, col) &&
+                    !qrCode.isDark(row + 1, col) &&
+                    qrCode.isDark(row + 2, col) &&
+                    qrCode.isDark(row + 3, col) &&
+                    qrCode.isDark(row + 4, col) &&
+                    !qrCode.isDark(row + 5, col) &&
+                    qrCode.isDark(row + 6, col)
+                ) {
+                    lostPoint += 40;
+                }
+            }
+        }
+        let darkCount = 0;
+        for (let col = 0; col < moduleCount; col++) {
+            for (let row = 0; row < moduleCount; row++) {
+                if (qrCode.isDark(row, col)) {
+                    darkCount++;
+                }
+            }
+        }
+        let ratio = Math.abs((100 * darkCount) / moduleCount / moduleCount - 50) / 5;
+        lostPoint += ratio * 10;
+        return lostPoint;
+    },
+};

+ 1 - 1
src/types/plugins/omemo/templates/profile.d.ts

@@ -1,3 +1,3 @@
-declare function _default(el: any): import("lit").TemplateResult<1>;
+declare function _default(el: import("../profile").Profile): import("lit").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=profile.d.ts.map

+ 1 - 0
src/types/plugins/omemo/utils.d.ts

@@ -1,4 +1,5 @@
 export function formatFingerprint(fp: any): any;
+export function formatFingerprintForQRCode(fp: any): string;
 /**
  * @param {Error|IQError|UserFacingError} e
  * @param {ChatBox} chat

+ 24 - 0
src/types/shared/qrcode/component.d.ts

@@ -0,0 +1,24 @@
+export default QRCodeComponent;
+declare class QRCodeComponent extends CustomElement {
+    static get properties(): {
+        text: {
+            type: StringConstructor;
+        };
+        width: {
+            type: StringConstructor;
+        };
+        height: {
+            type: StringConstructor;
+        };
+    };
+    text: any;
+    width: string;
+    height: string;
+    colorDark: string;
+    colorLight: string;
+    correctLevel: number;
+    render(): import("lit").TemplateResult<2>;
+    #private;
+}
+import { CustomElement } from "shared/components/element.js";
+//# sourceMappingURL=component.d.ts.map

+ 26 - 0
src/types/shared/qrcode/constants.d.ts

@@ -0,0 +1,26 @@
+export namespace QRErrorCorrectLevelMap {
+    let L: number;
+    let M: number;
+    let Q: number;
+    let H: number;
+}
+export namespace QRMode {
+    let MODE_NUMBER: number;
+    let MODE_ALPHA_NUM: number;
+    let MODE_8BIT_BYTE: number;
+    let MODE_KANJI: number;
+}
+export namespace QRMaskPattern {
+    let PATTERN000: number;
+    let PATTERN001: number;
+    let PATTERN010: number;
+    let PATTERN011: number;
+    let PATTERN100: number;
+    let PATTERN101: number;
+    let PATTERN110: number;
+    let PATTERN111: number;
+}
+export const QRCodeLimitLength: number[][];
+export const PATTERN_POSITION_TABLE: number[][];
+export const RS_BLOCK_TABLE: number[][];
+//# sourceMappingURL=constants.d.ts.map

+ 85 - 0
src/types/shared/qrcode/generator.d.ts

@@ -0,0 +1,85 @@
+export class QRCodeModel {
+    static get PAD0(): number;
+    static get PAD1(): number;
+    static createData(typeNumber: any, errorCorrectLevel: any, dataList: any): any[];
+    static createBytes(buffer: any, rsBlocks: any): any[];
+    /**
+     * @param {String} text
+     * @param {import('./types').ErrorCorrectLevel} errorCorrectLevel
+     */
+    constructor(text: string, errorCorrectLevel: import("./types").ErrorCorrectLevel);
+    text: string;
+    errorCorrectLevel: number;
+    typeNumber: number;
+    modules: any[];
+    moduleCount: number;
+    dataCache: any[];
+    dataList: QR8bitByte[];
+    /**
+     * @param {number} row
+     * @param {number} col
+     */
+    isDark(row: number, col: number): any;
+    getModuleCount(): number;
+    make(): void;
+    /**
+     * @param {boolean} test
+     * @param {Number} maskPattern
+     */
+    makeImpl(test: boolean, maskPattern: number): void;
+    /**
+     * @param {number} row
+     * @param {number} col
+     */
+    setupPositionProbePattern(row: number, col: number): void;
+    /**
+     * @returns {Number}
+     */
+    getBestMaskPattern(): number;
+    setupTimingPattern(): void;
+    setupPositionAdjustPattern(): void;
+    /**
+     * @param {boolean} test
+     */
+    setupTypeNumber(test: boolean): void;
+    /**
+     * @param {boolean} test
+     * @param {Number} maskPattern
+     */
+    setupTypeInfo(test: boolean, maskPattern: number): void;
+    mapData(data: any, maskPattern: any): void;
+}
+declare class QR8bitByte {
+    /**
+     * @param {string} data
+     */
+    constructor(data: string);
+    mode: number;
+    data: string;
+    parsedData: any;
+    getLength(): any;
+    /**
+     * @param {QRBitBuffer} buffer
+     */
+    write(buffer: QRBitBuffer): void;
+}
+declare class QRBitBuffer {
+    buffer: any[];
+    length: number;
+    /**
+     * @param {Number} index
+     */
+    get(index: number): boolean;
+    /**
+     * @param {Number} num
+     * @param {Number} length
+     */
+    put(num: number, length: number): void;
+    getLengthInBits(): number;
+    /**
+     * @param {Boolean} bit
+     */
+    putBit(bit: boolean): void;
+}
+export {};
+//# sourceMappingURL=generator.d.ts.map

+ 16 - 0
src/types/shared/qrcode/polynomial.d.ts

@@ -0,0 +1,16 @@
+export default QRPolynomial;
+export namespace QRMath {
+    function glog(n: any): any;
+    function gexp(n: any): any;
+    let EXP_TABLE: any[];
+    let LOG_TABLE: any[];
+}
+declare class QRPolynomial {
+    constructor(num: any, shift: any);
+    num: any[];
+    get(index: any): any;
+    getLength(): number;
+    multiply(e: any): QRPolynomial;
+    mod(e: any): any;
+}
+//# sourceMappingURL=polynomial.d.ts.map

+ 11 - 0
src/types/shared/qrcode/types.d.ts

@@ -0,0 +1,11 @@
+import { QRErrorCorrectLevelMap } from './constants';
+export type ErrorCorrectLevel = typeof QRErrorCorrectLevelMap[keyof typeof QRErrorCorrectLevelMap];
+export type VOption = {
+    text: string;
+    width?: number;
+    height?: number;
+    colorDark?: string;
+    colorLight?: string;
+    correctLevel?: ErrorCorrectLevel;
+};
+//# sourceMappingURL=types.d.ts.map

+ 22 - 0
src/types/shared/qrcode/utils.d.ts

@@ -0,0 +1,22 @@
+/**
+ * Get the type by string length
+ * @param {String} text
+ * @param {Number} nCorrectLevel
+ * @return {Number} type
+ */
+export function getTypeNumber(text: string, nCorrectLevel: number): number;
+export namespace QRUtil {
+    let G15: number;
+    let G18: number;
+    let G15_MASK: number;
+    function getBCHTypeInfo(data: any): number;
+    function getBCHTypeNumber(data: any): number;
+    function getBCHDigit(data: any): number;
+    function getPatternPosition(typeNumber: any): number[];
+    function getMask(maskPattern: any, i: any, j: any): boolean;
+    function getErrorCorrectPolynomial(errorCorrectLength: any): QRPolynomial;
+    function getLengthInBits(mode: any, type: any): 10 | 8 | 9 | 11 | 12 | 16 | 13 | 14;
+    function getLostPoint(qrCode: any): number;
+}
+import QRPolynomial from "./polynomial";
+//# sourceMappingURL=utils.d.ts.map