Render and sanitize chat messages server-side. (#237)
* Render and sanitize chat messages server-side. Closes #235 * Render content.md server-side and return it in the client config * Remove showdown from web project * Update api spec * Move example user content file
This commit is contained in:
parent
9eab6d7553
commit
d7c3991b59
192
build/javascript/package-lock.json
generated
192
build/javascript/package-lock.json
generated
@ -613,7 +613,8 @@
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true
|
||||
},
|
||||
"camelcase-css": {
|
||||
"version": "2.0.1",
|
||||
@ -702,31 +703,6 @@
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true
|
||||
},
|
||||
"cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
|
||||
"requires": {
|
||||
"string-width": "^3.1.0",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"wrap-ansi": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"clone-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||
@ -872,7 +848,8 @@
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
|
||||
"dev": true
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "6.0.0",
|
||||
@ -969,11 +946,6 @@
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz",
|
||||
"integrity": "sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
||||
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
@ -1142,14 +1114,6 @@
|
||||
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
|
||||
"dev": true
|
||||
},
|
||||
"find-up": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
|
||||
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||
"requires": {
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"focus-trap": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-5.1.0.tgz",
|
||||
@ -1211,11 +1175,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-1.1.4.tgz",
|
||||
"integrity": "sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ=="
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
@ -1459,11 +1418,6 @@
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
|
||||
@ -1570,15 +1524,6 @@
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||
"requires": {
|
||||
"p-locate": "^3.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
@ -1913,18 +1858,11 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
|
||||
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||
"requires": {
|
||||
"p-limit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-map": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||
@ -1956,7 +1894,8 @@
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
},
|
||||
"parent-module": {
|
||||
"version": "1.0.1",
|
||||
@ -1979,11 +1918,6 @@
|
||||
"lines-and-columns": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@ -2360,16 +2294,6 @@
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@ -2474,48 +2398,6 @@
|
||||
"rust-result": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"showdown": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz",
|
||||
"integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==",
|
||||
"requires": {
|
||||
"yargs": "^14.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"yargs": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
|
||||
"integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
|
||||
"requires": {
|
||||
"cliui": "^5.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^3.0.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^15.0.1"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "15.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
|
||||
"integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
@ -2758,31 +2640,6 @@
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
|
||||
"requires": {
|
||||
"emoji-regex": "^7.0.1",
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@ -3159,36 +3016,6 @@
|
||||
"global": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
||||
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.0",
|
||||
"string-width": "^3.0.0",
|
||||
"strip-ansi": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@ -3210,11 +3037,6 @@
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
|
||||
},
|
||||
"yaml": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
|
||||
|
@ -10,7 +10,6 @@
|
||||
"@videojs/themes": "^1.0.0",
|
||||
"htm": "^3.0.4",
|
||||
"preact": "^10.5.3",
|
||||
"showdown": "^1.9.1",
|
||||
"tailwindcss": "^1.8.10",
|
||||
"video.js": "^7.9.6"
|
||||
},
|
||||
@ -27,7 +26,6 @@
|
||||
"@justinribeiro/lite-youtube",
|
||||
"htm",
|
||||
"preact",
|
||||
"showdown",
|
||||
"tailwindcss/dist/tailwind.min.css"
|
||||
]
|
||||
},
|
||||
|
@ -28,15 +28,15 @@ type config struct {
|
||||
|
||||
// InstanceDetails defines the user-visible information about this particular instance.
|
||||
type InstanceDetails struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Title string `yaml:"title" json:"title"`
|
||||
Summary string `yaml:"summary" json:"summary"`
|
||||
Logo logo `yaml:"logo" json:"logo"`
|
||||
Tags []string `yaml:"tags" json:"tags"`
|
||||
SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"`
|
||||
ExtraInfoFile string `yaml:"extraUserInfoFileName" json:"extraUserInfoFileName"`
|
||||
Version string `json:"version"`
|
||||
NSFW bool `yaml:"nsfw" json:"nsfw"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Title string `yaml:"title" json:"title"`
|
||||
Summary string `yaml:"summary" json:"summary"`
|
||||
Logo logo `yaml:"logo" json:"logo"`
|
||||
Tags []string `yaml:"tags" json:"tags"`
|
||||
SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"`
|
||||
Version string `json:"version"`
|
||||
NSFW bool `yaml:"nsfw" json:"nsfw"`
|
||||
ExtraUserContent string `json:"extraUserContent"`
|
||||
}
|
||||
|
||||
type logo struct {
|
||||
@ -118,6 +118,13 @@ func (c *config) load(filePath string) error {
|
||||
|
||||
c.VideoSettings.HighestQualityStreamIndex = findHighestQuality(c.VideoSettings.StreamQualities)
|
||||
|
||||
// Add custom page content to the instance details.
|
||||
customContentMarkdownData, err := ioutil.ReadFile(ExtraInfoFile)
|
||||
if err == nil {
|
||||
customContentMarkdownString := string(customContentMarkdownData)
|
||||
c.InstanceDetails.ExtraUserContent = utils.RenderSimpleMarkdown(customContentMarkdownString)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -224,14 +231,5 @@ func Load(filePath string, versionInfo string) error {
|
||||
|
||||
Config.VersionInfo = versionInfo
|
||||
|
||||
// Defaults
|
||||
|
||||
// This is relative to the webroot, not the project root.
|
||||
// Has to be set here instead of pulled from a getter
|
||||
// since it's serialized to JSON.
|
||||
if Config.InstanceDetails.ExtraInfoFile == "" {
|
||||
Config.InstanceDetails.ExtraInfoFile = _default.InstanceDetails.ExtraInfoFile
|
||||
}
|
||||
|
||||
return Config.verifySettings()
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ const (
|
||||
WebRoot = "webroot"
|
||||
PrivateHLSStoragePath = "hls"
|
||||
GeoIPDatabasePath = "data/GeoLite2-City.mmdb"
|
||||
ExtraInfoFile = "data/content.md"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -13,7 +13,6 @@ func getDefaults() config {
|
||||
defaults.VideoSettings.ChunkLengthInSeconds = 4
|
||||
defaults.Files.MaxNumberInPlaylist = 5
|
||||
defaults.VideoSettings.OfflineContent = "static/offline.m4v"
|
||||
defaults.InstanceDetails.ExtraInfoFile = "/static/content.md"
|
||||
defaults.YP.Enabled = false
|
||||
defaults.YP.YPServiceURL = "https://yp.owncast.online"
|
||||
|
||||
|
33
core/chat/messageRendering_test.go
Normal file
33
core/chat/messageRendering_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
// Test a bunch of arbitrary markup and markdown to make sure we get sanitized
|
||||
// and fully rendered HTML out of it.
|
||||
func TestRenderAndSanitize(t *testing.T) {
|
||||
messageContent := `
|
||||
Test one two three! I go to http://yahoo.com and search for _sports_ and **answers**.
|
||||
Here is an iframe <iframe src="http://yahoo.com"></iframe>
|
||||
|
||||
## blah blah blah
|
||||
[test link](http://owncast.online)
|
||||
<img class="emoji" alt="bananadance.gif" width="600px" src="https://goth.land/img/emoji/bananadance.gif">
|
||||
<script src="http://hackers.org/hack.js"></script>
|
||||
`
|
||||
|
||||
expected := `<p>Test one two three! I go to <a href="http://yahoo.com" rel="nofollow noreferrer noopener" target="_blank">http://yahoo.com</a> and search for <em>sports</em> and <strong>answers</strong>.
|
||||
Here is an iframe </p>
|
||||
blah blah blah
|
||||
<p><a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a>
|
||||
<img class="emoji" alt="bananadance.gif" src="https://goth.land/img/emoji/bananadance.gif"></p>`
|
||||
|
||||
result := models.RenderAndSanitize(messageContent)
|
||||
if result != expected {
|
||||
t.Errorf("message rendering/sanitation does not match expected. Got\n%s, \n\n want:\n%s", result, expected)
|
||||
}
|
||||
|
||||
}
|
@ -110,10 +110,17 @@ func (s *server) Listen() {
|
||||
delete(s.Clients, c.socketID)
|
||||
s.listener.ClientRemoved(c.ClientID)
|
||||
|
||||
// broadcast a message to all clients
|
||||
// message was recieved from a client and should be sanitized, validated
|
||||
// and distributed to other clients.
|
||||
case msg := <-s.sendAllCh:
|
||||
// Will turn markdown into html, sanitize user-supplied raw html
|
||||
// and standardize this message into something safe we can send everyone else.
|
||||
msg.RenderAndSanitizeMessageBody()
|
||||
|
||||
s.listener.MessageSent(msg)
|
||||
s.sendAll(msg)
|
||||
|
||||
// Store in the message history
|
||||
addMessage(msg)
|
||||
case ping := <-s.pingCh:
|
||||
fmt.Println("PING?", ping)
|
||||
|
4
go.mod
4
go.mod
@ -8,7 +8,9 @@ require (
|
||||
github.com/aws/aws-sdk-go v1.34.0
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
github.com/mssola/user_agent v0.5.2
|
||||
github.com/mvdan/xurls v1.1.0 // indirect
|
||||
github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/oschwald/geoip2-golang v1.4.0
|
||||
@ -16,9 +18,11 @@ require (
|
||||
github.com/shirou/gopsutil v2.20.7+incompatible
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
|
||||
github.com/yuin/goldmark v1.2.1
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||
golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
mvdan.cc/xurls v1.1.0
|
||||
)
|
||||
|
15
go.sum
15
go.sum
@ -6,12 +6,18 @@ github.com/amalfra/etag v0.0.0-20190921100247-cafc8de96bc5/go.mod h1:Qk51jPgvIaO
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/aws/aws-sdk-go v1.34.0 h1:brux2dRrlwCF5JhTL7MUT3WUwo9zfDHZZp3+g3Mvlmo=
|
||||
github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
|
||||
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
@ -21,8 +27,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/mssola/user_agent v0.5.2 h1:CZkTUahjL1+OcZ5zv3kZr8QiJ8jy2H08vZIEkBeRbxo=
|
||||
github.com/mssola/user_agent v0.5.2/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
||||
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
|
||||
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
||||
github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88 h1:CXq5QLPMcfGEZMx8uBMyLdDiUNV72vlkSiyqg+jf7AI=
|
||||
github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
@ -50,9 +60,12 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@ -78,3 +91,5 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
|
||||
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
|
||||
|
@ -1,6 +1,16 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"mvdan.cc/xurls"
|
||||
)
|
||||
|
||||
//ChatMessage represents a single chat message
|
||||
type ChatMessage struct {
|
||||
@ -16,6 +26,91 @@ type ChatMessage struct {
|
||||
}
|
||||
|
||||
//Valid checks to ensure the message is valid
|
||||
func (s ChatMessage) Valid() bool {
|
||||
return s.Author != "" && s.Body != "" && s.ID != ""
|
||||
func (m ChatMessage) Valid() bool {
|
||||
return m.Author != "" && m.Body != "" && m.ID != ""
|
||||
}
|
||||
|
||||
// RenderAndSanitizeMessageBody will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
|
||||
// the message into something safe and renderable for clients.
|
||||
func (m *ChatMessage) RenderAndSanitizeMessageBody() {
|
||||
raw := m.Body
|
||||
|
||||
// Set the new, sanitized and rendered message body
|
||||
m.Body = RenderAndSanitize(raw)
|
||||
}
|
||||
|
||||
// RenderAndSanitize will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
|
||||
// the message into something safe and renderable for clients.
|
||||
func RenderAndSanitize(raw string) string {
|
||||
rendered := renderMarkdown(raw)
|
||||
safe := sanitize(rendered)
|
||||
|
||||
// Set the new, sanitized and rendered message body
|
||||
return strings.TrimSpace(safe)
|
||||
}
|
||||
|
||||
func renderMarkdown(raw string) string {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
extension.NewLinkify(
|
||||
extension.WithLinkifyAllowedProtocols([][]byte{
|
||||
[]byte("http:"),
|
||||
[]byte("https:"),
|
||||
}),
|
||||
extension.WithLinkifyURLRegexp(
|
||||
xurls.Strict,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
var buf bytes.Buffer
|
||||
if err := markdown.Convert([]byte(trimmed), &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func sanitize(raw string) string {
|
||||
p := bluemonday.StrictPolicy()
|
||||
|
||||
// Require URLs to be parseable by net/url.Parse
|
||||
p.AllowStandardURLs()
|
||||
|
||||
// Allow links
|
||||
p.AllowAttrs("href").OnElements("a")
|
||||
|
||||
// Force all URLs to have "noreferrer" in their rel attribute.
|
||||
p.RequireNoReferrerOnLinks(true)
|
||||
|
||||
// Links will get target="_blank" added to them.
|
||||
p.AddTargetBlankToFullyQualifiedLinks(true)
|
||||
|
||||
// Allow paragraphs
|
||||
p.AllowElements("br")
|
||||
p.AllowElements("p")
|
||||
|
||||
// Allow img tags
|
||||
p.AllowElements("img")
|
||||
p.AllowAttrs("src").OnElements("img")
|
||||
p.AllowAttrs("alt").OnElements("img")
|
||||
p.AllowAttrs("title").OnElements("img")
|
||||
|
||||
// Custom emoji have a class already specified.
|
||||
// We should only allow classes on emoji, not *all* imgs.
|
||||
// But TODO.
|
||||
p.AllowAttrs("class").OnElements("img")
|
||||
|
||||
// Allow bold
|
||||
p.AllowElements("strong")
|
||||
|
||||
// Allow emphasis
|
||||
p.AllowElements("em")
|
||||
|
||||
return p.Sanitize(raw)
|
||||
}
|
||||
|
@ -112,9 +112,10 @@ components:
|
||||
url:
|
||||
type: string
|
||||
example: http://github.com/owncast/owncast
|
||||
extraUserInfoFileName:
|
||||
extraUserContent:
|
||||
type: string
|
||||
description: Path to markdown file that has additional rich content about the server.
|
||||
description: Additional HTML content to render in the body of the web interface.
|
||||
example: "<p>This page is <strong>super</strong> cool!"
|
||||
version:
|
||||
type: string
|
||||
example: Owncast v0.0.2-macOS (ef3796a033b32a312ebf5b334851cbf9959e7ecb)
|
||||
@ -236,7 +237,7 @@ paths:
|
||||
/api/config:
|
||||
get:
|
||||
summary: Information
|
||||
description: Get the public information about the server. Adds context to the server, as well as information useful for the user interface.
|
||||
description: The client configuration. Information useful for the user interface.
|
||||
tags: ["Server"]
|
||||
responses:
|
||||
'200':
|
||||
|
@ -1,12 +1,17 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mssola/user_agent"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"mvdan.cc/xurls"
|
||||
)
|
||||
|
||||
//GetTemporaryPipePath gets the temporary path for the streampipe.flv file
|
||||
@ -65,3 +70,30 @@ func IsUserAgentABot(userAgent string) bool {
|
||||
ua := user_agent.New(userAgent)
|
||||
return ua.Bot()
|
||||
}
|
||||
|
||||
func RenderSimpleMarkdown(raw string) string {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
extension.NewLinkify(
|
||||
extension.WithLinkifyAllowedProtocols([][]byte{
|
||||
[]byte("http:"),
|
||||
[]byte("https:"),
|
||||
}),
|
||||
extension.WithLinkifyURLRegexp(
|
||||
xurls.Strict,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
var buf bytes.Buffer
|
||||
if err := markdown.Convert([]byte(trimmed), &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { h, Component } from '/js/web_modules/preact.js';
|
||||
import htm from '/js/web_modules/htm.js';
|
||||
const html = htm.bind(h);
|
||||
import showdown from '/js/web_modules/showdown.js';
|
||||
|
||||
import { OwncastPlayer } from './components/player.js';
|
||||
import SocialIconsList from './components/social-icons-list.js';
|
||||
@ -97,7 +96,6 @@ export default class App extends Component {
|
||||
// fetch events
|
||||
this.getConfig = this.getConfig.bind(this);
|
||||
this.getStreamStatus = this.getStreamStatus.bind(this);
|
||||
this.getExtraUserContent = this.getExtraUserContent.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -164,30 +162,9 @@ export default class App extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
// fetch content.md
|
||||
getExtraUserContent(path) {
|
||||
fetch(path)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok ${response.ok}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((text) => {
|
||||
this.setState({
|
||||
extraUserContent: new showdown.Converter().makeHtml(text),
|
||||
});
|
||||
})
|
||||
.catch((error) => {});
|
||||
}
|
||||
|
||||
setConfigData(data = {}) {
|
||||
const { title, extraUserInfoFileName, summary } = data;
|
||||
|
||||
const { title, summary } = data;
|
||||
window.document.title = title;
|
||||
if (extraUserInfoFileName) {
|
||||
this.getExtraUserContent(extraUserInfoFileName);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
configData: {
|
||||
@ -349,7 +326,6 @@ export default class App extends Component {
|
||||
chatInputEnabled,
|
||||
configData,
|
||||
displayChat,
|
||||
extraUserContent,
|
||||
orientation,
|
||||
playerActive,
|
||||
streamOnline,
|
||||
@ -371,6 +347,7 @@ export default class App extends Component {
|
||||
summary,
|
||||
tags = [],
|
||||
title,
|
||||
extraUserContent,
|
||||
} = configData;
|
||||
const {
|
||||
small: smallLogo = TEMP_IMAGE,
|
||||
|
@ -2,6 +2,8 @@ import { h, Component, createRef } from '/js/web_modules/preact.js';
|
||||
import htm from '/js/web_modules/htm.js';
|
||||
const html = htm.bind(h);
|
||||
|
||||
import '/js/web_modules/@justinribeiro/lite-youtube.js';
|
||||
|
||||
import Message from './message.js';
|
||||
import ChatInput from './chat-input.js';
|
||||
import { CALLBACKS, SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
|
||||
|
@ -3,15 +3,14 @@ import htm from '/js/web_modules/htm.js';
|
||||
const html = htm.bind(h);
|
||||
|
||||
import { messageBubbleColorForString } from '../../utils/user-colors.js';
|
||||
import { formatMessageText, formatTimestamp } from '../../utils/chat.js';
|
||||
import { generateAvatar } from '../../utils/helpers.js';
|
||||
import { convertToText } from '../../utils/chat.js';
|
||||
import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
|
||||
|
||||
export default class Message extends Component {
|
||||
render(props) {
|
||||
const { message, username } = props;
|
||||
const { type } = message;
|
||||
|
||||
if (type === SOCKET_MESSAGE_TYPES.CHAT) {
|
||||
const { image, author, body, timestamp } = message;
|
||||
const formattedMessage = formatMessageText(body, username);
|
||||
@ -66,3 +65,118 @@ export default class Message extends Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMessageText(message, username) {
|
||||
let formattedText = highlightUsername(message, username);
|
||||
formattedText = getMessageWithEmbeds(formattedText);
|
||||
return convertToMarkup(formattedText);
|
||||
}
|
||||
|
||||
function highlightUsername(message, username) {
|
||||
const pattern = new RegExp(
|
||||
'@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
'gi'
|
||||
);
|
||||
return message.replace(
|
||||
pattern,
|
||||
'<span class="highlighted px-1 rounded font-bold bg-orange-500">$&</span>'
|
||||
);
|
||||
}
|
||||
|
||||
function getMessageWithEmbeds(message) {
|
||||
var embedText = '';
|
||||
// Make a temporary element so we can actually parse the html and pull anchor tags from it.
|
||||
// This is a better approach than regex.
|
||||
var container = document.createElement('p');
|
||||
container.innerHTML = message;
|
||||
|
||||
var anchors = container.getElementsByTagName('a');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
const url = anchors[i].href;
|
||||
if (getYoutubeIdFromURL(url)) {
|
||||
const youtubeID = getYoutubeIdFromURL(url);
|
||||
embedText += getYoutubeEmbedFromID(youtubeID);
|
||||
} else if (url.indexOf('instagram.com/p/') > -1) {
|
||||
embedText += getInstagramEmbedFromURL(url);
|
||||
} else if (isImage(url)) {
|
||||
embedText += getImageForURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// If this message only consists of a single embeddable link
|
||||
// then only return the embed and strip the link url from the text.
|
||||
if (embedText !== '' && anchors.length == 1 && isMessageJustAnchor(message, anchors[0])) {
|
||||
return embedText;
|
||||
}
|
||||
return message + embedText;
|
||||
}
|
||||
|
||||
function getYoutubeIdFromURL(url) {
|
||||
try {
|
||||
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
|
||||
var match = url.match(regExp);
|
||||
|
||||
if (match && match[2].length == 11) {
|
||||
return match[2];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getYoutubeEmbedFromID(id) {
|
||||
return `<div class="chat-embed youtube-embed"><lite-youtube videoid="${id}" /></div>`;
|
||||
}
|
||||
|
||||
function getInstagramEmbedFromURL(url) {
|
||||
const urlObject = new URL(url.replace(/\/$/, ''));
|
||||
urlObject.pathname += '/embed';
|
||||
return `<iframe class="chat-embed instagram-embed" src="${urlObject.href}" frameborder="0" allowfullscreen></iframe>`;
|
||||
}
|
||||
|
||||
function isImage(url) {
|
||||
const re = /\.(jpe?g|png|gif)$/i;
|
||||
return re.test(url);
|
||||
}
|
||||
|
||||
function getImageForURL(url) {
|
||||
return `<a target="_blank" href="${url}"><img class="chat-embed embedded-image" src="${url}" /></a>`;
|
||||
}
|
||||
|
||||
function isMessageJustAnchor(message, anchor) {
|
||||
return stripTags(message) === stripTags(anchor.innerHTML);
|
||||
}
|
||||
|
||||
function formatTimestamp(sentAt) {
|
||||
sentAt = new Date(sentAt);
|
||||
if (isNaN(sentAt)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let diffInDays = (new Date() - sentAt) / (24 * 3600 * 1000);
|
||||
if (diffInDays >= 1) {
|
||||
return (
|
||||
`Sent at ${sentAt.toLocaleDateString('en-US', {
|
||||
dateStyle: 'medium',
|
||||
})} at ` + sentAt.toLocaleTimeString()
|
||||
);
|
||||
}
|
||||
|
||||
return `Sent at ${sentAt.toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
/*
|
||||
You would call this when receiving a plain text
|
||||
value back from an API, and before inserting the
|
||||
text into the `contenteditable` area on a page.
|
||||
*/
|
||||
function convertToMarkup(str = '') {
|
||||
return convertToText(str).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
function stripTags(str) {
|
||||
return str.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
}
|
||||
|
@ -4,134 +4,6 @@ import {
|
||||
CHAT_PLACEHOLDER_OFFLINE,
|
||||
} from './constants.js';
|
||||
|
||||
import showdown from '/js/web_modules/showdown.js';
|
||||
export function formatMessageText(message, username) {
|
||||
showdown.setFlavor('github');
|
||||
let formattedText = new showdown.Converter({
|
||||
emoji: true,
|
||||
openLinksInNewWindow: true,
|
||||
tables: false,
|
||||
simplifiedAutoLink: false,
|
||||
literalMidWordUnderscores: true,
|
||||
strikethrough: true,
|
||||
ghMentions: false,
|
||||
}).makeHtml(message);
|
||||
|
||||
formattedText = linkify(formattedText, message);
|
||||
formattedText = highlightUsername(formattedText, username);
|
||||
|
||||
return convertToMarkup(formattedText);
|
||||
}
|
||||
|
||||
function highlightUsername(message, username) {
|
||||
const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi');
|
||||
return message.replace(
|
||||
pattern,
|
||||
'<span class="highlighted px-1 rounded font-bold bg-orange-500">$&</span>'
|
||||
);
|
||||
}
|
||||
|
||||
function linkify(text, rawText) {
|
||||
const urls = getURLs(stripTags(rawText));
|
||||
if (urls) {
|
||||
urls.forEach(function (url) {
|
||||
let linkURL = url;
|
||||
|
||||
// Add http prefix if none exist in the URL so it actually
|
||||
// will work in an anchor tag.
|
||||
if (linkURL.indexOf('http') === -1) {
|
||||
linkURL = 'http://' + linkURL;
|
||||
}
|
||||
|
||||
// Remove the protocol prefix in the display URLs just to make
|
||||
// things look a little nicer.
|
||||
const displayURL = url.replace(/(^\w+:|^)\/\//, '');
|
||||
const link = `<a href="${linkURL}" target="_blank">${displayURL}</a>`;
|
||||
text = text.replace(url, link);
|
||||
|
||||
if (getYoutubeIdFromURL(url)) {
|
||||
if (isTextJustURLs(text, [url, displayURL])) {
|
||||
text = '';
|
||||
} else {
|
||||
text += '<br/>';
|
||||
}
|
||||
|
||||
const youtubeID = getYoutubeIdFromURL(url);
|
||||
text += getYoutubeEmbedFromID(youtubeID);
|
||||
} else if (url.indexOf('instagram.com/p/') > -1) {
|
||||
if (isTextJustURLs(text, [url, displayURL])) {
|
||||
text = '';
|
||||
} else {
|
||||
text += `<br/>`;
|
||||
}
|
||||
text += getInstagramEmbedFromURL(url);
|
||||
} else if (isImage(url)) {
|
||||
if (isTextJustURLs(text, [url, displayURL])) {
|
||||
text = '';
|
||||
} else {
|
||||
text += `<br/>`;
|
||||
}
|
||||
text += getImageForURL(url);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function isTextJustURLs(text, urls) {
|
||||
for (var i = 0; i < urls.length; i++) {
|
||||
const url = urls[i];
|
||||
if (stripTags(text) === url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function stripTags(str) {
|
||||
return str.replace(/<\/?[^>]+(>|$)/g, "");
|
||||
}
|
||||
|
||||
function getURLs(str) {
|
||||
var exp = /((\w+:\/\/\S+)|(\w+[\.:]\w+\S+))[^\s,\.]/ig;
|
||||
return str.match(exp);
|
||||
}
|
||||
|
||||
function getYoutubeIdFromURL(url) {
|
||||
try {
|
||||
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
|
||||
var match = url.match(regExp);
|
||||
|
||||
if (match && match[2].length == 11) {
|
||||
return match[2];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getYoutubeEmbedFromID(id) {
|
||||
return `<div class="chat-embed youtube-embed"><lite-youtube videoid="${id}" /></div>`;
|
||||
}
|
||||
|
||||
function getInstagramEmbedFromURL(url) {
|
||||
const urlObject = new URL(url.replace(/\/$/, ""));
|
||||
urlObject.pathname += "/embed";
|
||||
return `<iframe class="chat-embed instagram-embed" src="${urlObject.href}" frameborder="0" allowfullscreen></iframe>`;
|
||||
}
|
||||
|
||||
function isImage(url) {
|
||||
const re = /\.(jpe?g|png|gif)$/i;
|
||||
return re.test(url);
|
||||
}
|
||||
|
||||
function getImageForURL(url) {
|
||||
return `<a target="_blank" href="${url}"><img class="chat-embed embedded-image" src="${url}" /></a>`;
|
||||
}
|
||||
|
||||
// Taken from https://stackoverflow.com/questions/3972014/get-contenteditable-caret-index-position
|
||||
export function getCaretPosition(editableDiv) {
|
||||
@ -234,15 +106,6 @@ export function convertToText(str = '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
/*
|
||||
You would call this when receiving a plain text
|
||||
value back from an API, and before inserting the
|
||||
text into the `contenteditable` area on a page.
|
||||
*/
|
||||
export function convertToMarkup(str = '') {
|
||||
return convertToText(str).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
/*
|
||||
You would call this when a user pastes from
|
||||
the clipboard into a `contenteditable` area.
|
||||
@ -279,18 +142,3 @@ export function convertOnPaste( event = { preventDefault() {} }) {
|
||||
document.execCommand('insertText', false, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTimestamp(sentAt) {
|
||||
sentAt = new Date(sentAt);
|
||||
if (isNaN(sentAt)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000);
|
||||
if (diffInDays >= 1) {
|
||||
return `Sent at ${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` +
|
||||
sentAt.toLocaleTimeString();
|
||||
}
|
||||
|
||||
return `Sent at ${sentAt.toLocaleTimeString()}`;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { c as createCommonjsModule, a as commonjsGlobal, g as getDefaultExportFromCjs } from '../../../common/_commonjsHelpers-37fa8da4.js';
|
||||
import { d as document_1, w as window_1$1 } from '../../../common/window-2f8a9a85.js';
|
||||
import { c as createCommonjsModule, a as commonjsGlobal, d as document_1, w as window_1$1, g as getDefaultExportFromCjs } from '../../../common/window-1e586371.js';
|
||||
|
||||
var _extends_1 = createCommonjsModule(function (module) {
|
||||
function _extends() {
|
||||
|
66
webroot/js/web_modules/common/window-1e586371.js
Normal file
66
webroot/js/web_modules/common/window-1e586371.js
Normal file
@ -0,0 +1,66 @@
|
||||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
|
||||
function getDefaultExportFromCjs (x) {
|
||||
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
||||
}
|
||||
|
||||
function createCommonjsModule(fn, basedir, module) {
|
||||
return module = {
|
||||
path: basedir,
|
||||
exports: {},
|
||||
require: function (path, base) {
|
||||
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
|
||||
}
|
||||
}, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
function getDefaultExportFromNamespaceIfNotNamed (n) {
|
||||
return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n;
|
||||
}
|
||||
|
||||
function commonjsRequire () {
|
||||
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
|
||||
}
|
||||
|
||||
var _nodeResolve_empty = {};
|
||||
|
||||
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
|
||||
__proto__: null,
|
||||
'default': _nodeResolve_empty
|
||||
});
|
||||
|
||||
var minDoc = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(_nodeResolve_empty$1);
|
||||
|
||||
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
|
||||
typeof window !== 'undefined' ? window : {};
|
||||
|
||||
|
||||
var doccy;
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
doccy = document;
|
||||
} else {
|
||||
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
|
||||
|
||||
if (!doccy) {
|
||||
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
|
||||
}
|
||||
}
|
||||
|
||||
var document_1 = doccy;
|
||||
|
||||
var win;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
win = window;
|
||||
} else if (typeof commonjsGlobal !== "undefined") {
|
||||
win = commonjsGlobal;
|
||||
} else if (typeof self !== "undefined"){
|
||||
win = self;
|
||||
} else {
|
||||
win = {};
|
||||
}
|
||||
|
||||
var window_1 = win;
|
||||
|
||||
export { commonjsGlobal as a, createCommonjsModule as c, document_1 as d, getDefaultExportFromCjs as g, window_1 as w };
|
1
webroot/js/web_modules/import-map.json
vendored
1
webroot/js/web_modules/import-map.json
vendored
@ -6,7 +6,6 @@
|
||||
"@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css",
|
||||
"htm": "./htm.js",
|
||||
"preact": "./preact.js",
|
||||
"showdown": "./showdown.js",
|
||||
"tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css",
|
||||
"video.js/dist/video-js.min.css": "./videojs/dist/video-js.min.css",
|
||||
"video.js/dist/video.min.js": "./videojs/dist/video.min.js"
|
||||
|
5044
webroot/js/web_modules/showdown.js
vendored
5044
webroot/js/web_modules/showdown.js
vendored
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
||||
import { c as createCommonjsModule, a as commonjsGlobal } from '../../common/_commonjsHelpers-37fa8da4.js';
|
||||
import { w as window_1, d as document_1 } from '../../common/window-2f8a9a85.js';
|
||||
import { c as createCommonjsModule, w as window_1, d as document_1, a as commonjsGlobal } from '../../common/window-1e586371.js';
|
||||
|
||||
var video_min = createCommonjsModule(function (module, exports) {
|
||||
/**
|
||||
|
@ -152,6 +152,8 @@
|
||||
|
||||
|
||||
.message-text .emoji {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
width: 3rem;
|
||||
padding: .25rem
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user