@zaisma/game-server 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bench/brand/.assetsignore +4 -0
- package/bench/brand/README.md +51 -0
- package/bench/brand/_headers +13 -0
- package/bench/brand/app-icon-1024.png +0 -0
- package/bench/brand/app-icon-192.png +0 -0
- package/bench/brand/app-icon-512.png +0 -0
- package/bench/brand/apple-touch-icon.png +0 -0
- package/bench/brand/cover-guide/coin-cascade-busy-fine.webp +0 -0
- package/bench/brand/cover-guide/gem-grid-cover.webp +0 -0
- package/bench/brand/cover-guide/gem-grid-screenshot.webp +0 -0
- package/bench/brand/cover-guide/good-arena.webp +0 -0
- package/bench/brand/cover-guide/good-four-in-a-row.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-extra-text.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-fake-reward.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-fine.webp +0 -0
- package/bench/brand/cover-guide/lava-leap-overlay.webp +0 -0
- package/bench/brand/cover-guide/screenshot-fine.webp +0 -0
- package/bench/brand/cover-guide/screenshot-not-fine.webp +0 -0
- package/bench/brand/design/covers/ashen-crown.webp +0 -0
- package/bench/brand/design/covers/frostline.webp +0 -0
- package/bench/brand/design/covers/glasswing.webp +0 -0
- package/bench/brand/design/covers/hollow-tide.webp +0 -0
- package/bench/brand/design/covers/iron-vigil.webp +0 -0
- package/bench/brand/design/covers/kestrel.webp +0 -0
- package/bench/brand/design/covers/neon-drift.webp +0 -0
- package/bench/brand/design/covers/verdant.webp +0 -0
- package/bench/brand/favicon-32.png +0 -0
- package/bench/brand/favicon.ico +0 -0
- package/bench/brand/favicon.svg +1 -0
- package/bench/brand/flags/LICENSE +21 -0
- package/bench/brand/flags/ad.png +0 -0
- package/bench/brand/flags/ae.png +0 -0
- package/bench/brand/flags/af.png +0 -0
- package/bench/brand/flags/ag.png +0 -0
- package/bench/brand/flags/ai.png +0 -0
- package/bench/brand/flags/al.png +0 -0
- package/bench/brand/flags/am.png +0 -0
- package/bench/brand/flags/ao.png +0 -0
- package/bench/brand/flags/aq.png +0 -0
- package/bench/brand/flags/ar.png +0 -0
- package/bench/brand/flags/as.png +0 -0
- package/bench/brand/flags/at.png +0 -0
- package/bench/brand/flags/au.png +0 -0
- package/bench/brand/flags/aw.png +0 -0
- package/bench/brand/flags/ax.png +0 -0
- package/bench/brand/flags/az.png +0 -0
- package/bench/brand/flags/ba.png +0 -0
- package/bench/brand/flags/bb.png +0 -0
- package/bench/brand/flags/bd.png +0 -0
- package/bench/brand/flags/be.png +0 -0
- package/bench/brand/flags/bf.png +0 -0
- package/bench/brand/flags/bg.png +0 -0
- package/bench/brand/flags/bh.png +0 -0
- package/bench/brand/flags/bi.png +0 -0
- package/bench/brand/flags/bj.png +0 -0
- package/bench/brand/flags/bl.png +0 -0
- package/bench/brand/flags/bm.png +0 -0
- package/bench/brand/flags/bn.png +0 -0
- package/bench/brand/flags/bo.png +0 -0
- package/bench/brand/flags/bq.png +0 -0
- package/bench/brand/flags/br.png +0 -0
- package/bench/brand/flags/bs.png +0 -0
- package/bench/brand/flags/bt.png +0 -0
- package/bench/brand/flags/bv.png +0 -0
- package/bench/brand/flags/bw.png +0 -0
- package/bench/brand/flags/by.png +0 -0
- package/bench/brand/flags/bz.png +0 -0
- package/bench/brand/flags/ca.png +0 -0
- package/bench/brand/flags/cc.png +0 -0
- package/bench/brand/flags/cd.png +0 -0
- package/bench/brand/flags/cf.png +0 -0
- package/bench/brand/flags/cg.png +0 -0
- package/bench/brand/flags/ch.png +0 -0
- package/bench/brand/flags/ci.png +0 -0
- package/bench/brand/flags/ck.png +0 -0
- package/bench/brand/flags/cl.png +0 -0
- package/bench/brand/flags/cm.png +0 -0
- package/bench/brand/flags/cn.png +0 -0
- package/bench/brand/flags/co.png +0 -0
- package/bench/brand/flags/cp.png +0 -0
- package/bench/brand/flags/cr.png +0 -0
- package/bench/brand/flags/cu.png +0 -0
- package/bench/brand/flags/cv.png +0 -0
- package/bench/brand/flags/cw.png +0 -0
- package/bench/brand/flags/cx.png +0 -0
- package/bench/brand/flags/cy.png +0 -0
- package/bench/brand/flags/cz.png +0 -0
- package/bench/brand/flags/de.png +0 -0
- package/bench/brand/flags/dg.png +0 -0
- package/bench/brand/flags/dj.png +0 -0
- package/bench/brand/flags/dk.png +0 -0
- package/bench/brand/flags/dm.png +0 -0
- package/bench/brand/flags/do.png +0 -0
- package/bench/brand/flags/dz.png +0 -0
- package/bench/brand/flags/ec.png +0 -0
- package/bench/brand/flags/ee.png +0 -0
- package/bench/brand/flags/eg.png +0 -0
- package/bench/brand/flags/eh.png +0 -0
- package/bench/brand/flags/er.png +0 -0
- package/bench/brand/flags/es.png +0 -0
- package/bench/brand/flags/et.png +0 -0
- package/bench/brand/flags/eu.png +0 -0
- package/bench/brand/flags/fi.png +0 -0
- package/bench/brand/flags/fj.png +0 -0
- package/bench/brand/flags/fk.png +0 -0
- package/bench/brand/flags/fm.png +0 -0
- package/bench/brand/flags/fo.png +0 -0
- package/bench/brand/flags/fr.png +0 -0
- package/bench/brand/flags/ga.png +0 -0
- package/bench/brand/flags/gb.png +0 -0
- package/bench/brand/flags/gd.png +0 -0
- package/bench/brand/flags/ge.png +0 -0
- package/bench/brand/flags/gf.png +0 -0
- package/bench/brand/flags/gg.png +0 -0
- package/bench/brand/flags/gh.png +0 -0
- package/bench/brand/flags/gi.png +0 -0
- package/bench/brand/flags/gl.png +0 -0
- package/bench/brand/flags/gm.png +0 -0
- package/bench/brand/flags/gn.png +0 -0
- package/bench/brand/flags/gp.png +0 -0
- package/bench/brand/flags/gq.png +0 -0
- package/bench/brand/flags/gr.png +0 -0
- package/bench/brand/flags/gs.png +0 -0
- package/bench/brand/flags/gt.png +0 -0
- package/bench/brand/flags/gu.png +0 -0
- package/bench/brand/flags/gw.png +0 -0
- package/bench/brand/flags/gy.png +0 -0
- package/bench/brand/flags/hk.png +0 -0
- package/bench/brand/flags/hm.png +0 -0
- package/bench/brand/flags/hn.png +0 -0
- package/bench/brand/flags/hr.png +0 -0
- package/bench/brand/flags/ht.png +0 -0
- package/bench/brand/flags/hu.png +0 -0
- package/bench/brand/flags/ic.png +0 -0
- package/bench/brand/flags/id.png +0 -0
- package/bench/brand/flags/ie.png +0 -0
- package/bench/brand/flags/il.png +0 -0
- package/bench/brand/flags/im.png +0 -0
- package/bench/brand/flags/in.png +0 -0
- package/bench/brand/flags/io.png +0 -0
- package/bench/brand/flags/iq.png +0 -0
- package/bench/brand/flags/ir.png +0 -0
- package/bench/brand/flags/is.png +0 -0
- package/bench/brand/flags/it.png +0 -0
- package/bench/brand/flags/je.png +0 -0
- package/bench/brand/flags/jm.png +0 -0
- package/bench/brand/flags/jo.png +0 -0
- package/bench/brand/flags/jp.png +0 -0
- package/bench/brand/flags/ke.png +0 -0
- package/bench/brand/flags/kg.png +0 -0
- package/bench/brand/flags/kh.png +0 -0
- package/bench/brand/flags/ki.png +0 -0
- package/bench/brand/flags/km.png +0 -0
- package/bench/brand/flags/kn.png +0 -0
- package/bench/brand/flags/kp.png +0 -0
- package/bench/brand/flags/kr.png +0 -0
- package/bench/brand/flags/kw.png +0 -0
- package/bench/brand/flags/ky.png +0 -0
- package/bench/brand/flags/kz.png +0 -0
- package/bench/brand/flags/la.png +0 -0
- package/bench/brand/flags/lb.png +0 -0
- package/bench/brand/flags/lc.png +0 -0
- package/bench/brand/flags/li.png +0 -0
- package/bench/brand/flags/lk.png +0 -0
- package/bench/brand/flags/lr.png +0 -0
- package/bench/brand/flags/ls.png +0 -0
- package/bench/brand/flags/lt.png +0 -0
- package/bench/brand/flags/lu.png +0 -0
- package/bench/brand/flags/lv.png +0 -0
- package/bench/brand/flags/ly.png +0 -0
- package/bench/brand/flags/ma.png +0 -0
- package/bench/brand/flags/mc.png +0 -0
- package/bench/brand/flags/md.png +0 -0
- package/bench/brand/flags/me.png +0 -0
- package/bench/brand/flags/mf.png +0 -0
- package/bench/brand/flags/mg.png +0 -0
- package/bench/brand/flags/mh.png +0 -0
- package/bench/brand/flags/mk.png +0 -0
- package/bench/brand/flags/ml.png +0 -0
- package/bench/brand/flags/mm.png +0 -0
- package/bench/brand/flags/mn.png +0 -0
- package/bench/brand/flags/mo.png +0 -0
- package/bench/brand/flags/mp.png +0 -0
- package/bench/brand/flags/mq.png +0 -0
- package/bench/brand/flags/mr.png +0 -0
- package/bench/brand/flags/ms.png +0 -0
- package/bench/brand/flags/mt.png +0 -0
- package/bench/brand/flags/mu.png +0 -0
- package/bench/brand/flags/mv.png +0 -0
- package/bench/brand/flags/mw.png +0 -0
- package/bench/brand/flags/mx.png +0 -0
- package/bench/brand/flags/my.png +0 -0
- package/bench/brand/flags/mz.png +0 -0
- package/bench/brand/flags/na.png +0 -0
- package/bench/brand/flags/nc.png +0 -0
- package/bench/brand/flags/ne.png +0 -0
- package/bench/brand/flags/nf.png +0 -0
- package/bench/brand/flags/ng.png +0 -0
- package/bench/brand/flags/ni.png +0 -0
- package/bench/brand/flags/nl.png +0 -0
- package/bench/brand/flags/no.png +0 -0
- package/bench/brand/flags/np.png +0 -0
- package/bench/brand/flags/nr.png +0 -0
- package/bench/brand/flags/nu.png +0 -0
- package/bench/brand/flags/nz.png +0 -0
- package/bench/brand/flags/om.png +0 -0
- package/bench/brand/flags/pa.png +0 -0
- package/bench/brand/flags/pc.png +0 -0
- package/bench/brand/flags/pe.png +0 -0
- package/bench/brand/flags/pf.png +0 -0
- package/bench/brand/flags/pg.png +0 -0
- package/bench/brand/flags/ph.png +0 -0
- package/bench/brand/flags/pk.png +0 -0
- package/bench/brand/flags/pl.png +0 -0
- package/bench/brand/flags/pm.png +0 -0
- package/bench/brand/flags/pn.png +0 -0
- package/bench/brand/flags/pr.png +0 -0
- package/bench/brand/flags/ps.png +0 -0
- package/bench/brand/flags/pt.png +0 -0
- package/bench/brand/flags/pw.png +0 -0
- package/bench/brand/flags/py.png +0 -0
- package/bench/brand/flags/qa.png +0 -0
- package/bench/brand/flags/re.png +0 -0
- package/bench/brand/flags/ro.png +0 -0
- package/bench/brand/flags/rs.png +0 -0
- package/bench/brand/flags/ru.png +0 -0
- package/bench/brand/flags/rw.png +0 -0
- package/bench/brand/flags/sa.png +0 -0
- package/bench/brand/flags/sb.png +0 -0
- package/bench/brand/flags/sc.png +0 -0
- package/bench/brand/flags/sd.png +0 -0
- package/bench/brand/flags/se.png +0 -0
- package/bench/brand/flags/sg.png +0 -0
- package/bench/brand/flags/sh.png +0 -0
- package/bench/brand/flags/si.png +0 -0
- package/bench/brand/flags/sj.png +0 -0
- package/bench/brand/flags/sk.png +0 -0
- package/bench/brand/flags/sl.png +0 -0
- package/bench/brand/flags/sm.png +0 -0
- package/bench/brand/flags/sn.png +0 -0
- package/bench/brand/flags/so.png +0 -0
- package/bench/brand/flags/sr.png +0 -0
- package/bench/brand/flags/ss.png +0 -0
- package/bench/brand/flags/st.png +0 -0
- package/bench/brand/flags/sv.png +0 -0
- package/bench/brand/flags/sx.png +0 -0
- package/bench/brand/flags/sy.png +0 -0
- package/bench/brand/flags/sz.png +0 -0
- package/bench/brand/flags/tc.png +0 -0
- package/bench/brand/flags/td.png +0 -0
- package/bench/brand/flags/tf.png +0 -0
- package/bench/brand/flags/tg.png +0 -0
- package/bench/brand/flags/th.png +0 -0
- package/bench/brand/flags/tj.png +0 -0
- package/bench/brand/flags/tk.png +0 -0
- package/bench/brand/flags/tl.png +0 -0
- package/bench/brand/flags/tm.png +0 -0
- package/bench/brand/flags/tn.png +0 -0
- package/bench/brand/flags/to.png +0 -0
- package/bench/brand/flags/tr.png +0 -0
- package/bench/brand/flags/tt.png +0 -0
- package/bench/brand/flags/tv.png +0 -0
- package/bench/brand/flags/tw.png +0 -0
- package/bench/brand/flags/tz.png +0 -0
- package/bench/brand/flags/ua.png +0 -0
- package/bench/brand/flags/ug.png +0 -0
- package/bench/brand/flags/um.png +0 -0
- package/bench/brand/flags/un.png +0 -0
- package/bench/brand/flags/us.png +0 -0
- package/bench/brand/flags/uy.png +0 -0
- package/bench/brand/flags/uz.png +0 -0
- package/bench/brand/flags/va.png +0 -0
- package/bench/brand/flags/vc.png +0 -0
- package/bench/brand/flags/ve.png +0 -0
- package/bench/brand/flags/vg.png +0 -0
- package/bench/brand/flags/vi.png +0 -0
- package/bench/brand/flags/vn.png +0 -0
- package/bench/brand/flags/vu.png +0 -0
- package/bench/brand/flags/wf.png +0 -0
- package/bench/brand/flags/ws.png +0 -0
- package/bench/brand/flags/xk.png +0 -0
- package/bench/brand/flags/xx.png +0 -0
- package/bench/brand/flags/ye.png +0 -0
- package/bench/brand/flags/yt.png +0 -0
- package/bench/brand/flags/za.png +0 -0
- package/bench/brand/flags/zm.png +0 -0
- package/bench/brand/flags/zw.png +0 -0
- package/bench/brand/fonts/OFL.txt +93 -0
- package/bench/brand/fonts/jost.7726a5cd.woff2 +0 -0
- package/bench/brand/loader/gif/zaisma-loader-black-128.gif +0 -0
- package/bench/brand/loader/gif/zaisma-loader-black-512.gif +0 -0
- package/bench/brand/loader/gif/zaisma-loader-white-128.gif +0 -0
- package/bench/brand/loader/gif/zaisma-loader-white-512.gif +0 -0
- package/bench/brand/loader/png/zaisma-loader-black-128.png +0 -0
- package/bench/brand/loader/png/zaisma-loader-black-512.png +0 -0
- package/bench/brand/loader/png/zaisma-loader-white-128.png +0 -0
- package/bench/brand/loader/png/zaisma-loader-white-512.png +0 -0
- package/bench/brand/loader/webp/zaisma-loader-black-128.webp +0 -0
- package/bench/brand/loader/webp/zaisma-loader-black-512.webp +0 -0
- package/bench/brand/loader/webp/zaisma-loader-white-128.webp +0 -0
- package/bench/brand/loader/webp/zaisma-loader-white-512.webp +0 -0
- package/bench/brand/loader/zaisma-loader-black.svg +1 -0
- package/bench/brand/loader/zaisma-loader-white.svg +1 -0
- package/bench/brand/loader/zaisma-loader.svg +1 -0
- package/bench/brand/sign-in/city-day-phone.webp +0 -0
- package/bench/brand/sign-in/city-day.webp +0 -0
- package/bench/brand/sign-in/city-night-phone.webp +0 -0
- package/bench/brand/sign-in/city-night.webp +0 -0
- package/bench/brand/zaisma-app-icon.svg +1 -0
- package/bench/brand/zaisma-mark-black-2048.png +0 -0
- package/bench/brand/zaisma-mark-black.svg +1 -0
- package/bench/brand/zaisma-mark-metal-2048.png +0 -0
- package/bench/brand/zaisma-mark-metal.svg +1 -0
- package/bench/brand/zaisma-mark-small-black.svg +1 -0
- package/bench/brand/zaisma-mark-small-white.svg +1 -0
- package/bench/brand/zaisma-mark-tricolor.svg +1 -0
- package/bench/brand/zaisma-mark-white-2048.png +0 -0
- package/bench/brand/zaisma-mark-white.svg +1 -0
- package/bench/brand/zaisma-wordmark-black-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-black.svg +1 -0
- package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-metal.svg +1 -0
- package/bench/brand/zaisma-wordmark-white-2400.png +0 -0
- package/bench/brand/zaisma-wordmark-white.svg +1 -0
- package/bench/glass.js +46486 -0
- package/bin/local-play-bench.d.ts +8 -0
- package/bin/local-play-bench.js +218 -0
- package/bin/local-play-certificate.js +99 -0
- package/bin/local-play-runner.js +43 -0
- package/bin/local-play.d.ts +41 -0
- package/bin/local-play.js +1573 -0
- package/bin/node.js +99 -5
- package/package.json +1 -1
- package/sdk/picture.js +97 -0
- package/sdk/relay.js +269 -0
- package/sdk/saves.js +884 -0
- package/sdk/zaisma.d.ts +1810 -0
- package/sdk/zaisma.js +2459 -0
- package/src/chat.d.ts +23 -8
- package/src/chat.js +21 -11
- package/src/index.d.ts +2 -2
- package/src/index.js +1 -1
- package/src/pass.d.ts +9 -8
- package/src/pass.js +10 -8
- package/src/qr-code.d.ts +20 -0
- package/src/qr-code.js +319 -0
- package/src/room.d.ts +148 -3
- package/src/room.js +204 -3
package/src/chat.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
|
|
3
3
|
* leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
|
|
4
|
-
* contact details in any disguise, moving to another app or meeting up, personal details
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* contact details in any disguise, moving to another app or meeting up, personal details and sexual
|
|
5
|
+
* content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
|
|
6
|
+
* call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
|
|
7
|
+
* The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
|
|
7
8
|
*
|
|
8
9
|
* checkChat sends the players' passes (as your server received them) and the sender's last few messages to
|
|
9
10
|
* Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
|
|
@@ -11,14 +12,17 @@
|
|
|
11
12
|
* It's for your own server only. In Zaisma rooms the room filters the chat it delivers itself, so a game
|
|
12
13
|
* server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
|
|
13
14
|
*/
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Why a message was stopped. The same reasons as Zaisma.checkChat, plus chat_paused, not_allowed_to_chat and
|
|
17
|
+
* unchecked (Zaisma couldn't be reached). slur is v1's and no longer given: slurs are flagged (hasSlur), never stopped.
|
|
18
|
+
*/
|
|
15
19
|
export type ChatCheckReason = "contact_details" | "personal_details" | "slur" | "sexual" | "chat_paused" | "unchecked" | "too_long" | "not_allowed_to_chat";
|
|
16
20
|
export interface ChatCheck {
|
|
17
21
|
/** Whether to deliver it. */
|
|
18
22
|
isAllowed: boolean;
|
|
19
23
|
/**
|
|
20
|
-
* Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked:
|
|
21
|
-
* couldn't
|
|
24
|
+
* Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked: your
|
|
25
|
+
* server couldn't reach Zaisma, so don't deliver it (try again in a moment). too_long: over 2000 characters.
|
|
22
26
|
* not_allowed_to_chat: canChat says the sender and a receiver may not chat (Zaisma checks it too).
|
|
23
27
|
*/
|
|
24
28
|
reason: ChatCheckReason | null;
|
|
@@ -26,6 +30,8 @@ export interface ChatCheck {
|
|
|
26
30
|
message: string | null;
|
|
27
31
|
/** Whether it swears. Zaisma allows it; block it yourself if your game doesn't. */
|
|
28
32
|
hasSwearing: boolean;
|
|
33
|
+
/** Whether it has a slur. Zaisma allows it too, so your game and its moderators decide. */
|
|
34
|
+
hasSlur: boolean;
|
|
29
35
|
}
|
|
30
36
|
export interface ChatCheckOptions {
|
|
31
37
|
/** The message. */
|
|
@@ -34,7 +40,15 @@ export interface ChatCheckOptions {
|
|
|
34
40
|
senderPass: string;
|
|
35
41
|
/** The passes of everyone it would reach, up to 50. */
|
|
36
42
|
receiverPasses: string[];
|
|
43
|
+
/** Check chat between adults too, as for under-18s; each message Zaisma's AI looks at counts on your hosting meter. False by default. */
|
|
44
|
+
isAdultFilterEnabled?: boolean;
|
|
37
45
|
}
|
|
46
|
+
/** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
|
|
47
|
+
export declare const CHAT_ROOM_LIMITS: {
|
|
48
|
+
/** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
|
|
49
|
+
readonly maximumChatMessageCount: 20;
|
|
50
|
+
readonly chatWindowDurationMs: 10000;
|
|
51
|
+
};
|
|
38
52
|
export declare const CHAT_CHECK_LIMITS: {
|
|
39
53
|
/** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
|
|
40
54
|
readonly maximumMessageCharacterCount: 2000;
|
|
@@ -56,8 +70,9 @@ export declare class ChatCheckError extends Error {
|
|
|
56
70
|
/**
|
|
57
71
|
* Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
|
|
58
72
|
* canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
|
|
59
|
-
* too, so a message to anyone who may not chat is stopped). It's free
|
|
60
|
-
* reached it answers unchecked: don't deliver the message.
|
|
73
|
+
* too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
|
|
74
|
+
* adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
|
|
75
|
+
* The runner (zaisma-game-server) does all this for room.chat in its rooms.
|
|
61
76
|
*
|
|
62
77
|
* const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
|
|
63
78
|
* if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
|
package/src/chat.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
|
|
3
3
|
* leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
|
|
4
|
-
* contact details in any disguise, moving to another app or meeting up, personal details
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* contact details in any disguise, moving to another app or meeting up, personal details and sexual
|
|
5
|
+
* content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
|
|
6
|
+
* call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
|
|
7
|
+
* The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
|
|
7
8
|
*
|
|
8
9
|
* checkChat sends the players' passes (as your server received them) and the sender's last few messages to
|
|
9
10
|
* Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
|
|
@@ -12,6 +13,12 @@
|
|
|
12
13
|
* server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
|
|
13
14
|
*/
|
|
14
15
|
import { OutsideCallError } from "./outside-calls.js";
|
|
16
|
+
/** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
|
|
17
|
+
export const CHAT_ROOM_LIMITS = {
|
|
18
|
+
/** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
|
|
19
|
+
maximumChatMessageCount: 20,
|
|
20
|
+
chatWindowDurationMs: 10_000,
|
|
21
|
+
};
|
|
15
22
|
export const CHAT_CHECK_LIMITS = {
|
|
16
23
|
/** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
|
|
17
24
|
maximumMessageCharacterCount: 2000,
|
|
@@ -34,9 +41,9 @@ export class ChatCheckError extends Error {
|
|
|
34
41
|
this.fix = fix;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
|
-
const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false };
|
|
38
|
-
const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false };
|
|
39
|
-
const REASONS = ["contact_details", "personal_details", "
|
|
44
|
+
const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false, hasSlur: false };
|
|
45
|
+
const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false, hasSlur: false };
|
|
46
|
+
const REASONS = ["contact_details", "personal_details", "sexual", "chat_paused", "unchecked", "too_long", "not_allowed_to_chat"];
|
|
40
47
|
/** Each sender's last messages, by the sender's pass's player ID: only in this process's memory. */
|
|
41
48
|
const recentBySender = new Map();
|
|
42
49
|
/** The claims of a pass, unchecked: Zaisma checks the signature. Only to find whose it is and where it's from. */
|
|
@@ -67,13 +74,16 @@ function rememberText(senderPlayerId, text, nowMs) {
|
|
|
67
74
|
/**
|
|
68
75
|
* Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
|
|
69
76
|
* canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
|
|
70
|
-
* too, so a message to anyone who may not chat is stopped). It's free
|
|
71
|
-
* reached it answers unchecked: don't deliver the message.
|
|
77
|
+
* too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
|
|
78
|
+
* adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
|
|
79
|
+
* The runner (zaisma-game-server) does all this for room.chat in its rooms.
|
|
72
80
|
*
|
|
73
81
|
* const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
|
|
74
82
|
* if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
|
|
75
83
|
*/
|
|
76
84
|
export async function checkChat({ text, senderPass, receiverPasses }) {
|
|
85
|
+
// Read apart from the destructuring, which is v1's signature as it's locked (test/contracts).
|
|
86
|
+
const isAdultFilterEnabled = arguments[0]?.isAdultFilterEnabled === true;
|
|
77
87
|
const claims = readClaims(String(senderPass ?? ""));
|
|
78
88
|
if (typeof claims.sub !== "string" || !isZaismaIssuer(claims.iss) || !Array.isArray(receiverPasses))
|
|
79
89
|
return UNCHECKED;
|
|
@@ -87,7 +97,7 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
|
|
|
87
97
|
const response = await fetch(`${claims.iss}/v1/chat/check`, {
|
|
88
98
|
method: "POST",
|
|
89
99
|
headers: { "Content-Type": "application/json" },
|
|
90
|
-
body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts }),
|
|
100
|
+
body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts, isAdultFilterEnabled }),
|
|
91
101
|
signal: AbortSignal.timeout(CHAT_CHECK_LIMITS.checkTimeoutMs),
|
|
92
102
|
});
|
|
93
103
|
if (!response.ok)
|
|
@@ -96,12 +106,12 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
|
|
|
96
106
|
if (typeof answer.isAllowed !== "boolean")
|
|
97
107
|
return UNCHECKED;
|
|
98
108
|
const reason = typeof answer.reason === "string" && REASONS.includes(answer.reason) ? answer.reason : null;
|
|
99
|
-
return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true };
|
|
109
|
+
return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true, hasSlur: answer.hasSlur === true };
|
|
100
110
|
}
|
|
101
111
|
catch (error) {
|
|
102
112
|
// In Zaisma rooms the gateway refuses the call to Zaisma's own address: checkChat is for a game server on your own server.
|
|
103
113
|
if (error instanceof OutsideCallError && ["unreachable_host", "not_https", "host_not_allowed"].includes(error.code)) {
|
|
104
|
-
throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "
|
|
114
|
+
throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "In Zaisma rooms, send chat with room.chat in the game; the room filters it.");
|
|
105
115
|
}
|
|
106
116
|
return UNCHECKED;
|
|
107
117
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
|
|
1
|
+
export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type ChatAnswer, type ChatInfo, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
|
|
2
2
|
export { REQUEST_LIMITS, type RequestErrorCode, type Server } from "./requests.ts";
|
|
3
3
|
export { SCHEDULE_LIMITS, type Schedule, type ScheduledRun } from "./schedules.ts";
|
|
4
4
|
export { WEBHOOK_LIMITS, verifyWebhook, type WebhookError, type WebhookErrorCode, type WebhookEvent, type WebhookEventKind, type WebhookHeaders } from "./webhooks.ts";
|
|
5
5
|
export { DATABASE_LIMITS, DatabaseError, type DatabaseErrorCode, type DatabaseRow, type DatabaseStatement, type DatabaseValue, type GameDatabase } from "./database.ts";
|
|
6
6
|
export { OUTSIDE_CALL_LIMITS, OutsideCallError, type OutsideCallErrorCode } from "./outside-calls.ts";
|
|
7
7
|
export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt, type PassClaims, type PassErrorCode, type PassExpectations, type PlayerKind, type PublicKey, type ReceiptClaims, type ReceiptErrorCode, type ReceiptExpectations, type WhoCanChat } from "./pass.ts";
|
|
8
|
-
export { CHAT_CHECK_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
|
|
8
|
+
export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
|
|
9
9
|
export { guestName, isRoomCode } from "./names.ts";
|
|
10
10
|
export { relay } from "./relay.ts";
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,6 @@ export { WEBHOOK_LIMITS, verifyWebhook } from "./webhooks.js";
|
|
|
7
7
|
export { DATABASE_LIMITS, DatabaseError } from "./database.js";
|
|
8
8
|
export { OUTSIDE_CALL_LIMITS, OutsideCallError } from "./outside-calls.js";
|
|
9
9
|
export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt } from "./pass.js";
|
|
10
|
-
export { CHAT_CHECK_LIMITS, ChatCheckError, checkChat } from "./chat.js";
|
|
10
|
+
export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat } from "./chat.js";
|
|
11
11
|
export { guestName, isRoomCode } from "./names.js";
|
|
12
12
|
export { relay } from "./relay.js";
|
package/src/pass.d.ts
CHANGED
|
@@ -28,9 +28,9 @@ export interface PassClaims {
|
|
|
28
28
|
*/
|
|
29
29
|
who_can_chat?: WhoCanChat | (string & {});
|
|
30
30
|
/**
|
|
31
|
-
* For accounts that chat
|
|
32
|
-
* chat when their groups match (canChat). Opaque and different in every
|
|
33
|
-
* age; a game could guess which group is the younger one from its size.
|
|
31
|
+
* For accounts that chat at all (friends or anyone), the group they can chat in (chat_group_…): two
|
|
32
|
+
* players chat only when their groups match (canChat), friends included. Opaque and different in every
|
|
33
|
+
* game, so it never says an age; a game could guess which group is the younger one from its size.
|
|
34
34
|
*/
|
|
35
35
|
chat_group_id?: string;
|
|
36
36
|
/**
|
|
@@ -88,11 +88,12 @@ export declare function passesHaveBlockBetween(first: PassClaims, second: PassCl
|
|
|
88
88
|
*/
|
|
89
89
|
export declare function passesAreFriends(first: PassClaims, second: PassClaims): boolean;
|
|
90
90
|
/**
|
|
91
|
-
* Whether two players may chat, by the passes your server checked (verifyPass): both accounts
|
|
92
|
-
* between them, neither letting nobody chat, and either friends or both letting
|
|
93
|
-
* chat
|
|
94
|
-
* friends (passesAreFriends). Use it on every message your
|
|
95
|
-
* chat check (checkChat) applies the same rule, and
|
|
91
|
+
* Whether two players may chat, by the passes your server checked (verifyPass): both accounts in the same
|
|
92
|
+
* chat group, no block between them, neither letting nobody chat, and either friends or both letting
|
|
93
|
+
* anyone chat. Guests and accounts set to nobody never chat; accounts set to friends chat only with
|
|
94
|
+
* friends (passesAreFriends); friends in different chat groups don't chat. Use it on every message your
|
|
95
|
+
* own server delivers, text or voice; Zaisma's chat check (checkChat) applies the same rule, and so do
|
|
96
|
+
* rooms (room.chat), on Zaisma and in the runner.
|
|
96
97
|
*/
|
|
97
98
|
export declare function canChat(first: PassClaims, second: PassClaims): boolean;
|
|
98
99
|
export type PublicKey = JsonWebKey & {
|
package/src/pass.js
CHANGED
|
@@ -44,11 +44,12 @@ export function passesAreFriends(first, second) {
|
|
|
44
44
|
return lists(first, second) && lists(second, first);
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Whether two players may chat, by the passes your server checked (verifyPass): both accounts
|
|
48
|
-
* between them, neither letting nobody chat, and either friends or both letting
|
|
49
|
-
* chat
|
|
50
|
-
* friends (passesAreFriends). Use it on every message your
|
|
51
|
-
* chat check (checkChat) applies the same rule, and
|
|
47
|
+
* Whether two players may chat, by the passes your server checked (verifyPass): both accounts in the same
|
|
48
|
+
* chat group, no block between them, neither letting nobody chat, and either friends or both letting
|
|
49
|
+
* anyone chat. Guests and accounts set to nobody never chat; accounts set to friends chat only with
|
|
50
|
+
* friends (passesAreFriends); friends in different chat groups don't chat. Use it on every message your
|
|
51
|
+
* own server delivers, text or voice; Zaisma's chat check (checkChat) applies the same rule, and so do
|
|
52
|
+
* rooms (room.chat), on Zaisma and in the runner.
|
|
52
53
|
*/
|
|
53
54
|
export function canChat(first, second) {
|
|
54
55
|
mustBePasses(first, second, "canChat");
|
|
@@ -59,9 +60,10 @@ export function canChat(first, second) {
|
|
|
59
60
|
const isOpenTo = (whoCanChat) => whoCanChat === "friends" || whoCanChat === "anyone";
|
|
60
61
|
if (!isOpenTo(first.who_can_chat) || !isOpenTo(second.who_can_chat))
|
|
61
62
|
return false;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Chat never leaves a chat group, friends included.
|
|
64
|
+
if (typeof first.chat_group_id !== "string" || first.chat_group_id !== second.chat_group_id)
|
|
65
|
+
return false;
|
|
66
|
+
return passesAreFriends(first, second) || (first.who_can_chat === "anyone" && second.who_can_chat === "anyone");
|
|
65
67
|
}
|
|
66
68
|
// Plain class fields (no TypeScript-only syntax) so Node can run this file directly.
|
|
67
69
|
export class PassError extends Error {
|
package/src/qr-code.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR codes for game and room links (docs/DECISIONS.md, "QR codes"): players get one behind Share,
|
|
3
|
+
* developers on the dashboard and through the API and MCP. Byte mode, error correction level M,
|
|
4
|
+
* versions 1 to 10 (up to 213 bytes, far more than a link needs). Written here rather than taken from a
|
|
5
|
+
* package so the site's page and the API draw the same code with no dependency; it follows ISO/IEC
|
|
6
|
+
* 18004 the way Project Nayuki's reference encoder does.
|
|
7
|
+
*/
|
|
8
|
+
export declare const QR_CODE_LIMITS: {
|
|
9
|
+
/** Version 10 at level M holds 213 bytes. */
|
|
10
|
+
readonly maximumTextBytes: 213;
|
|
11
|
+
/** Light modules around the code, as the standard asks. */
|
|
12
|
+
readonly quietZoneModuleCount: 4;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The code's modules for `text`, as rows of dark (true) and light (false), with no quiet zone. Throws
|
|
16
|
+
* a RangeError when the text is over QR_CODE_LIMITS.maximumTextBytes in UTF-8.
|
|
17
|
+
*/
|
|
18
|
+
export declare function qrCodeModules(text: string): boolean[][];
|
|
19
|
+
/** The code for `text` as an SVG document: dark modules on white, with the quiet zone, one unit a module. */
|
|
20
|
+
export declare function qrCodeSvg(text: string): string;
|
package/src/qr-code.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR codes for game and room links (docs/DECISIONS.md, "QR codes"): players get one behind Share,
|
|
3
|
+
* developers on the dashboard and through the API and MCP. Byte mode, error correction level M,
|
|
4
|
+
* versions 1 to 10 (up to 213 bytes, far more than a link needs). Written here rather than taken from a
|
|
5
|
+
* package so the site's page and the API draw the same code with no dependency; it follows ISO/IEC
|
|
6
|
+
* 18004 the way Project Nayuki's reference encoder does.
|
|
7
|
+
*/
|
|
8
|
+
export const QR_CODE_LIMITS = {
|
|
9
|
+
/** Version 10 at level M holds 213 bytes. */
|
|
10
|
+
maximumTextBytes: 213,
|
|
11
|
+
/** Light modules around the code, as the standard asks. */
|
|
12
|
+
quietZoneModuleCount: 4,
|
|
13
|
+
};
|
|
14
|
+
// Level M: error correction codewords per block and blocks, by version (index 0 unused).
|
|
15
|
+
const ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK = [0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26];
|
|
16
|
+
const ERROR_CORRECTION_BLOCK_COUNTS = [0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5];
|
|
17
|
+
/** Error correction level M (medium)'s two bits in the format information. */
|
|
18
|
+
const MEDIUM_CORRECTION_FORMAT_NUMBER = 0;
|
|
19
|
+
const HIGHEST_VERSION_NUMBER = 10;
|
|
20
|
+
const isBitSet = (bitsNumber, bitIndex) => ((bitsNumber >>> bitIndex) & 1) !== 0;
|
|
21
|
+
function rawDataModuleCount(versionNumber) {
|
|
22
|
+
let moduleCount = (16 * versionNumber + 128) * versionNumber + 64;
|
|
23
|
+
if (versionNumber >= 2) {
|
|
24
|
+
const alignmentCount = Math.floor(versionNumber / 7) + 2;
|
|
25
|
+
moduleCount -= (25 * alignmentCount - 10) * alignmentCount - 55;
|
|
26
|
+
if (versionNumber >= 7)
|
|
27
|
+
moduleCount -= 36;
|
|
28
|
+
}
|
|
29
|
+
return moduleCount;
|
|
30
|
+
}
|
|
31
|
+
const dataCodewordCount = (versionNumber) => Math.floor(rawDataModuleCount(versionNumber) / 8) - ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK[versionNumber] * ERROR_CORRECTION_BLOCK_COUNTS[versionNumber];
|
|
32
|
+
// Reed-Solomon over GF(256) with the polynomial 0x11D.
|
|
33
|
+
function multiplied(leftNumber, rightNumber) {
|
|
34
|
+
let productNumber = 0;
|
|
35
|
+
for (let bitIndex = 7; bitIndex >= 0; bitIndex--) {
|
|
36
|
+
productNumber = (productNumber << 1) ^ ((productNumber >>> 7) * 0x11d);
|
|
37
|
+
productNumber ^= ((rightNumber >>> bitIndex) & 1) * leftNumber;
|
|
38
|
+
}
|
|
39
|
+
return productNumber;
|
|
40
|
+
}
|
|
41
|
+
function divisorOf(degreeCount) {
|
|
42
|
+
const coefficients = new Array(degreeCount).fill(0);
|
|
43
|
+
coefficients[degreeCount - 1] = 1;
|
|
44
|
+
let rootNumber = 1;
|
|
45
|
+
for (let stepIndex = 0; stepIndex < degreeCount; stepIndex++) {
|
|
46
|
+
for (let index = 0; index < coefficients.length; index++) {
|
|
47
|
+
coefficients[index] = multiplied(coefficients[index], rootNumber);
|
|
48
|
+
if (index + 1 < coefficients.length)
|
|
49
|
+
coefficients[index] = coefficients[index] ^ coefficients[index + 1];
|
|
50
|
+
}
|
|
51
|
+
rootNumber = multiplied(rootNumber, 0x02);
|
|
52
|
+
}
|
|
53
|
+
return coefficients;
|
|
54
|
+
}
|
|
55
|
+
function remainderOf(codewords, divisor) {
|
|
56
|
+
const remainders = divisor.map(() => 0);
|
|
57
|
+
for (const codewordNumber of codewords) {
|
|
58
|
+
const factorNumber = codewordNumber ^ remainders.shift();
|
|
59
|
+
remainders.push(0);
|
|
60
|
+
divisor.forEach((coefficientNumber, index) => (remainders[index] = remainders[index] ^ multiplied(coefficientNumber, factorNumber)));
|
|
61
|
+
}
|
|
62
|
+
return remainders;
|
|
63
|
+
}
|
|
64
|
+
/** The data codewords with their error correction, split into blocks and interleaved. */
|
|
65
|
+
function withErrorCorrection(dataCodewords, versionNumber) {
|
|
66
|
+
const blockCount = ERROR_CORRECTION_BLOCK_COUNTS[versionNumber];
|
|
67
|
+
const blockCorrectionCount = ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK[versionNumber];
|
|
68
|
+
const rawCodewordCount = Math.floor(rawDataModuleCount(versionNumber) / 8);
|
|
69
|
+
const shortBlockCount = blockCount - (rawCodewordCount % blockCount);
|
|
70
|
+
const shortBlockCodewordCount = Math.floor(rawCodewordCount / blockCount);
|
|
71
|
+
const divisor = divisorOf(blockCorrectionCount);
|
|
72
|
+
const blocks = [];
|
|
73
|
+
for (let blockIndex = 0, startIndex = 0; blockIndex < blockCount; blockIndex++) {
|
|
74
|
+
const block = dataCodewords.slice(startIndex, startIndex + shortBlockCodewordCount - blockCorrectionCount + (blockIndex < shortBlockCount ? 0 : 1));
|
|
75
|
+
startIndex += block.length;
|
|
76
|
+
const correction = remainderOf(block, divisor);
|
|
77
|
+
if (blockIndex < shortBlockCount)
|
|
78
|
+
block.push(0);
|
|
79
|
+
blocks.push(block.concat(correction));
|
|
80
|
+
}
|
|
81
|
+
const interleaved = [];
|
|
82
|
+
for (let codewordIndex = 0; codewordIndex < blocks[0].length; codewordIndex++) {
|
|
83
|
+
blocks.forEach((block, blockIndex) => {
|
|
84
|
+
if (codewordIndex !== shortBlockCodewordCount - blockCorrectionCount || blockIndex >= shortBlockCount)
|
|
85
|
+
interleaved.push(block[codewordIndex]);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return interleaved;
|
|
89
|
+
}
|
|
90
|
+
function alignmentIndexes(versionNumber, sideModuleCount) {
|
|
91
|
+
if (versionNumber === 1)
|
|
92
|
+
return [];
|
|
93
|
+
const alignmentCount = Math.floor(versionNumber / 7) + 2;
|
|
94
|
+
const stepModuleCount = Math.ceil((versionNumber * 4 + 4) / (alignmentCount * 2 - 2)) * 2;
|
|
95
|
+
const indexes = [6];
|
|
96
|
+
for (let moduleIndex = sideModuleCount - 7; indexes.length < alignmentCount; moduleIndex -= stepModuleCount)
|
|
97
|
+
indexes.splice(1, 0, moduleIndex);
|
|
98
|
+
return indexes;
|
|
99
|
+
}
|
|
100
|
+
/** The standard's eight masks, by column and row. */
|
|
101
|
+
const MASKS = [
|
|
102
|
+
(columnIndex, rowIndex) => (columnIndex + rowIndex) % 2 === 0,
|
|
103
|
+
(...indexes) => indexes[1] % 2 === 0,
|
|
104
|
+
(columnIndex) => columnIndex % 3 === 0,
|
|
105
|
+
(columnIndex, rowIndex) => (columnIndex + rowIndex) % 3 === 0,
|
|
106
|
+
(columnIndex, rowIndex) => (Math.floor(columnIndex / 3) + Math.floor(rowIndex / 2)) % 2 === 0,
|
|
107
|
+
(columnIndex, rowIndex) => ((columnIndex * rowIndex) % 2) + ((columnIndex * rowIndex) % 3) === 0,
|
|
108
|
+
(columnIndex, rowIndex) => (((columnIndex * rowIndex) % 2) + ((columnIndex * rowIndex) % 3)) % 2 === 0,
|
|
109
|
+
(columnIndex, rowIndex) => (((columnIndex + rowIndex) % 2) + ((columnIndex * rowIndex) % 3)) % 2 === 0,
|
|
110
|
+
];
|
|
111
|
+
/** A score for how hard a masked code is to read (lower is better), from the standard's rules. */
|
|
112
|
+
function penaltyPointCount(modules) {
|
|
113
|
+
const sideModuleCount = modules.length;
|
|
114
|
+
let pointCount = 0;
|
|
115
|
+
const scoreLines = (isDarkAt) => {
|
|
116
|
+
for (let lineIndex = 0; lineIndex < sideModuleCount; lineIndex++) {
|
|
117
|
+
let runModuleCount = 0;
|
|
118
|
+
let isRunDark = false;
|
|
119
|
+
const line = [];
|
|
120
|
+
for (let alongIndex = 0; alongIndex < sideModuleCount; alongIndex++) {
|
|
121
|
+
const isDark = isDarkAt(lineIndex, alongIndex);
|
|
122
|
+
line.push(isDark);
|
|
123
|
+
if (alongIndex > 0 && isDark === isRunDark) {
|
|
124
|
+
runModuleCount++;
|
|
125
|
+
if (runModuleCount === 5)
|
|
126
|
+
pointCount += 3;
|
|
127
|
+
else if (runModuleCount > 5)
|
|
128
|
+
pointCount++;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
isRunDark = isDark;
|
|
132
|
+
runModuleCount = 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Finder-like runs: dark-light-dark-dark-dark-light-dark with four light modules on a side.
|
|
136
|
+
const padded = [false, false, false, false, ...line, false, false, false, false];
|
|
137
|
+
for (let startIndex = 0; startIndex + 11 <= padded.length; startIndex++) {
|
|
138
|
+
const isDarkAtOffset = (offsetIndex) => padded[startIndex + offsetIndex];
|
|
139
|
+
if (!(isDarkAtOffset(4) && !isDarkAtOffset(5) && isDarkAtOffset(6) && isDarkAtOffset(7) && isDarkAtOffset(8) && !isDarkAtOffset(9) && isDarkAtOffset(10)))
|
|
140
|
+
continue;
|
|
141
|
+
const isLightBefore = !isDarkAtOffset(0) && !isDarkAtOffset(1) && !isDarkAtOffset(2) && !isDarkAtOffset(3);
|
|
142
|
+
const isLightAfter = startIndex + 15 <= padded.length && !isDarkAtOffset(11) && !isDarkAtOffset(12) && !isDarkAtOffset(13) && !isDarkAtOffset(14);
|
|
143
|
+
if (isLightBefore || isLightAfter)
|
|
144
|
+
pointCount += 40;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
scoreLines((rowIndex, columnIndex) => modules[rowIndex][columnIndex]);
|
|
149
|
+
scoreLines((columnIndex, rowIndex) => modules[rowIndex][columnIndex]);
|
|
150
|
+
for (let rowIndex = 0; rowIndex + 1 < sideModuleCount; rowIndex++) {
|
|
151
|
+
for (let columnIndex = 0; columnIndex + 1 < sideModuleCount; columnIndex++) {
|
|
152
|
+
const isDark = modules[rowIndex][columnIndex];
|
|
153
|
+
if (isDark === modules[rowIndex][columnIndex + 1] && isDark === modules[rowIndex + 1][columnIndex] && isDark === modules[rowIndex + 1][columnIndex + 1])
|
|
154
|
+
pointCount += 3;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const darkModuleCount = modules.flat().filter(Boolean).length;
|
|
158
|
+
const moduleCount = sideModuleCount * sideModuleCount;
|
|
159
|
+
pointCount += (Math.ceil(Math.abs(darkModuleCount * 20 - moduleCount * 10) / moduleCount) - 1) * 10;
|
|
160
|
+
return pointCount;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The code's modules for `text`, as rows of dark (true) and light (false), with no quiet zone. Throws
|
|
164
|
+
* a RangeError when the text is over QR_CODE_LIMITS.maximumTextBytes in UTF-8.
|
|
165
|
+
*/
|
|
166
|
+
export function qrCodeModules(text) {
|
|
167
|
+
const textBytes = [...new TextEncoder().encode(text)];
|
|
168
|
+
const neededBitCount = (candidateNumber) => 4 + (candidateNumber < 10 ? 8 : 16) + textBytes.length * 8;
|
|
169
|
+
let versionNumber = 1;
|
|
170
|
+
while (versionNumber <= HIGHEST_VERSION_NUMBER && neededBitCount(versionNumber) > dataCodewordCount(versionNumber) * 8)
|
|
171
|
+
versionNumber++;
|
|
172
|
+
if (versionNumber > HIGHEST_VERSION_NUMBER)
|
|
173
|
+
throw new RangeError(`The text is ${textBytes.length} bytes; a QR code here holds at most ${QR_CODE_LIMITS.maximumTextBytes}.`);
|
|
174
|
+
// The bit stream: byte mode, the length, the bytes, a terminator, then padding.
|
|
175
|
+
const capacityBitCount = dataCodewordCount(versionNumber) * 8;
|
|
176
|
+
const bits = [];
|
|
177
|
+
const append = (bitsNumber, bitCount) => {
|
|
178
|
+
for (let bitIndex = bitCount - 1; bitIndex >= 0; bitIndex--)
|
|
179
|
+
bits.push((bitsNumber >>> bitIndex) & 1);
|
|
180
|
+
};
|
|
181
|
+
append(0b0100, 4);
|
|
182
|
+
append(textBytes.length, versionNumber < 10 ? 8 : 16);
|
|
183
|
+
for (const textByte of textBytes)
|
|
184
|
+
append(textByte, 8);
|
|
185
|
+
append(0, Math.min(4, capacityBitCount - bits.length));
|
|
186
|
+
append(0, (8 - (bits.length % 8)) % 8);
|
|
187
|
+
for (let padByte = 0xec; bits.length < capacityBitCount; padByte ^= 0xec ^ 0x11)
|
|
188
|
+
append(padByte, 8);
|
|
189
|
+
const dataCodewords = [];
|
|
190
|
+
for (let bitIndex = 0; bitIndex < bits.length; bitIndex += 8)
|
|
191
|
+
dataCodewords.push(bits.slice(bitIndex, bitIndex + 8).reduce((byteNumber, bitNumber) => (byteNumber << 1) | bitNumber, 0));
|
|
192
|
+
const codewords = withErrorCorrection(dataCodewords, versionNumber);
|
|
193
|
+
const sideModuleCount = versionNumber * 4 + 17;
|
|
194
|
+
const modules = Array.from({ length: sideModuleCount }, () => new Array(sideModuleCount).fill(false));
|
|
195
|
+
const isFunction = Array.from({ length: sideModuleCount }, () => new Array(sideModuleCount).fill(false));
|
|
196
|
+
const setFunction = (columnIndex, rowIndex, isDark) => {
|
|
197
|
+
modules[rowIndex][columnIndex] = isDark;
|
|
198
|
+
isFunction[rowIndex][columnIndex] = true;
|
|
199
|
+
};
|
|
200
|
+
for (let moduleIndex = 0; moduleIndex < sideModuleCount; moduleIndex++) {
|
|
201
|
+
setFunction(6, moduleIndex, moduleIndex % 2 === 0);
|
|
202
|
+
setFunction(moduleIndex, 6, moduleIndex % 2 === 0);
|
|
203
|
+
}
|
|
204
|
+
const finderCenters = [[3, 3], [sideModuleCount - 4, 3], [3, sideModuleCount - 4]];
|
|
205
|
+
for (const [centerColumnIndex, centerRowIndex] of finderCenters) {
|
|
206
|
+
for (let rowOffsetIndex = -4; rowOffsetIndex <= 4; rowOffsetIndex++) {
|
|
207
|
+
for (let columnOffsetIndex = -4; columnOffsetIndex <= 4; columnOffsetIndex++) {
|
|
208
|
+
const columnIndex = centerColumnIndex + columnOffsetIndex;
|
|
209
|
+
const rowIndex = centerRowIndex + rowOffsetIndex;
|
|
210
|
+
const ringIndex = Math.max(Math.abs(columnOffsetIndex), Math.abs(rowOffsetIndex));
|
|
211
|
+
if (columnIndex >= 0 && columnIndex < sideModuleCount && rowIndex >= 0 && rowIndex < sideModuleCount)
|
|
212
|
+
setFunction(columnIndex, rowIndex, ringIndex !== 2 && ringIndex !== 4);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const alignments = alignmentIndexes(versionNumber, sideModuleCount);
|
|
217
|
+
const lastAlignmentIndex = alignments.length - 1;
|
|
218
|
+
alignments.forEach((centerColumnIndex, alignmentColumnIndex) => alignments.forEach((centerRowIndex, alignmentRowIndex) => {
|
|
219
|
+
const isCorner = (alignmentColumnIndex === 0 && alignmentRowIndex === 0) || (alignmentColumnIndex === 0 && alignmentRowIndex === lastAlignmentIndex) || (alignmentColumnIndex === lastAlignmentIndex && alignmentRowIndex === 0);
|
|
220
|
+
if (isCorner)
|
|
221
|
+
return;
|
|
222
|
+
for (let rowOffsetIndex = -2; rowOffsetIndex <= 2; rowOffsetIndex++) {
|
|
223
|
+
for (let columnOffsetIndex = -2; columnOffsetIndex <= 2; columnOffsetIndex++) {
|
|
224
|
+
setFunction(centerColumnIndex + columnOffsetIndex, centerRowIndex + rowOffsetIndex, Math.max(Math.abs(columnOffsetIndex), Math.abs(rowOffsetIndex)) !== 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}));
|
|
228
|
+
const drawFormat = (maskIndex) => {
|
|
229
|
+
const formatNumber = (MEDIUM_CORRECTION_FORMAT_NUMBER << 3) | maskIndex;
|
|
230
|
+
let remainderNumber = formatNumber;
|
|
231
|
+
for (let stepIndex = 0; stepIndex < 10; stepIndex++)
|
|
232
|
+
remainderNumber = (remainderNumber << 1) ^ ((remainderNumber >>> 9) * 0x537);
|
|
233
|
+
const formatBitsNumber = ((formatNumber << 10) | remainderNumber) ^ 0x5412;
|
|
234
|
+
for (let bitIndex = 0; bitIndex <= 5; bitIndex++)
|
|
235
|
+
setFunction(8, bitIndex, isBitSet(formatBitsNumber, bitIndex));
|
|
236
|
+
setFunction(8, 7, isBitSet(formatBitsNumber, 6));
|
|
237
|
+
setFunction(8, 8, isBitSet(formatBitsNumber, 7));
|
|
238
|
+
setFunction(7, 8, isBitSet(formatBitsNumber, 8));
|
|
239
|
+
for (let bitIndex = 9; bitIndex < 15; bitIndex++)
|
|
240
|
+
setFunction(14 - bitIndex, 8, isBitSet(formatBitsNumber, bitIndex));
|
|
241
|
+
for (let bitIndex = 0; bitIndex < 8; bitIndex++)
|
|
242
|
+
setFunction(sideModuleCount - 1 - bitIndex, 8, isBitSet(formatBitsNumber, bitIndex));
|
|
243
|
+
for (let bitIndex = 8; bitIndex < 15; bitIndex++)
|
|
244
|
+
setFunction(8, sideModuleCount - 15 + bitIndex, isBitSet(formatBitsNumber, bitIndex));
|
|
245
|
+
setFunction(8, sideModuleCount - 8, true);
|
|
246
|
+
};
|
|
247
|
+
drawFormat(0);
|
|
248
|
+
if (versionNumber >= 7) {
|
|
249
|
+
let remainderNumber = versionNumber;
|
|
250
|
+
for (let stepIndex = 0; stepIndex < 12; stepIndex++)
|
|
251
|
+
remainderNumber = (remainderNumber << 1) ^ ((remainderNumber >>> 11) * 0x1f25);
|
|
252
|
+
const versionBitsNumber = (versionNumber << 12) | remainderNumber;
|
|
253
|
+
for (let bitIndex = 0; bitIndex < 18; bitIndex++) {
|
|
254
|
+
const farIndex = sideModuleCount - 11 + (bitIndex % 3);
|
|
255
|
+
const nearIndex = Math.floor(bitIndex / 3);
|
|
256
|
+
setFunction(farIndex, nearIndex, isBitSet(versionBitsNumber, bitIndex));
|
|
257
|
+
setFunction(nearIndex, farIndex, isBitSet(versionBitsNumber, bitIndex));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// The codewords, zigzagging up and down two columns at a time from the bottom right.
|
|
261
|
+
let codewordBitIndex = 0;
|
|
262
|
+
for (let rightColumnIndex = sideModuleCount - 1; rightColumnIndex >= 1; rightColumnIndex -= 2) {
|
|
263
|
+
if (rightColumnIndex === 6)
|
|
264
|
+
rightColumnIndex = 5;
|
|
265
|
+
for (let stepIndex = 0; stepIndex < sideModuleCount; stepIndex++) {
|
|
266
|
+
for (let pairIndex = 0; pairIndex < 2; pairIndex++) {
|
|
267
|
+
const columnIndex = rightColumnIndex - pairIndex;
|
|
268
|
+
const rowIndex = ((rightColumnIndex + 1) & 2) === 0 ? sideModuleCount - 1 - stepIndex : stepIndex;
|
|
269
|
+
if (!isFunction[rowIndex][columnIndex] && codewordBitIndex < codewords.length * 8) {
|
|
270
|
+
modules[rowIndex][columnIndex] = isBitSet(codewords[codewordBitIndex >>> 3], 7 - (codewordBitIndex & 7));
|
|
271
|
+
codewordBitIndex++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// The mask that reads best.
|
|
277
|
+
const masked = (maskIndex) => {
|
|
278
|
+
const copy = modules.map((row) => row.slice());
|
|
279
|
+
for (let rowIndex = 0; rowIndex < sideModuleCount; rowIndex++) {
|
|
280
|
+
for (let columnIndex = 0; columnIndex < sideModuleCount; columnIndex++) {
|
|
281
|
+
if (!isFunction[rowIndex][columnIndex] && MASKS[maskIndex](columnIndex, rowIndex))
|
|
282
|
+
copy[rowIndex][columnIndex] = !copy[rowIndex][columnIndex];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return copy;
|
|
286
|
+
};
|
|
287
|
+
let bestMaskIndex = 0;
|
|
288
|
+
let bestPointCount = Infinity;
|
|
289
|
+
for (let maskIndex = 0; maskIndex < MASKS.length; maskIndex++) {
|
|
290
|
+
drawFormat(maskIndex);
|
|
291
|
+
const pointCount = penaltyPointCount(masked(maskIndex));
|
|
292
|
+
if (pointCount < bestPointCount) {
|
|
293
|
+
bestMaskIndex = maskIndex;
|
|
294
|
+
bestPointCount = pointCount;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
drawFormat(bestMaskIndex);
|
|
298
|
+
return masked(bestMaskIndex);
|
|
299
|
+
}
|
|
300
|
+
/** The code for `text` as an SVG document: dark modules on white, with the quiet zone, one unit a module. */
|
|
301
|
+
export function qrCodeSvg(text) {
|
|
302
|
+
const modules = qrCodeModules(text);
|
|
303
|
+
const marginModuleCount = QR_CODE_LIMITS.quietZoneModuleCount;
|
|
304
|
+
const sideModuleCount = modules.length + marginModuleCount * 2;
|
|
305
|
+
let path = "";
|
|
306
|
+
modules.forEach((row, rowIndex) => {
|
|
307
|
+
for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
|
|
308
|
+
if (!row[columnIndex])
|
|
309
|
+
continue;
|
|
310
|
+
let runEndIndex = columnIndex;
|
|
311
|
+
while (runEndIndex + 1 < row.length && row[runEndIndex + 1])
|
|
312
|
+
runEndIndex++;
|
|
313
|
+
const runModuleCount = runEndIndex - columnIndex + 1;
|
|
314
|
+
path += `M${columnIndex + marginModuleCount} ${rowIndex + marginModuleCount}h${runModuleCount}v1h-${runModuleCount}z`;
|
|
315
|
+
columnIndex = runEndIndex;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sideModuleCount} ${sideModuleCount}" shape-rendering="crispEdges"><rect width="${sideModuleCount}" height="${sideModuleCount}" fill="#fff"/><path d="${path}" fill="#000"/></svg>`;
|
|
319
|
+
}
|