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:
Gabe Kangas 2020-10-13 16:45:52 -07:00 committed by GitHub
parent 9eab6d7553
commit d7c3991b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 408 additions and 5441 deletions

View File

@ -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",

View File

@ -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"
]
},

View File

@ -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()
}

View File

@ -6,6 +6,7 @@ const (
WebRoot = "webroot"
PrivateHLSStoragePath = "hls"
GeoIPDatabasePath = "data/GeoLite2-City.mmdb"
ExtraInfoFile = "data/content.md"
)
var (

View File

@ -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"

View 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)
}
}

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)
}

View File

@ -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':

View File

@ -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()
}

View File

@ -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,

View File

@ -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';

View File

@ -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, '');
}

View File

@ -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()}`;
}

View File

@ -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() {

View 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 };

View File

@ -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"

File diff suppressed because it is too large Load Diff

View File

@ -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) {
/**

View File

@ -152,6 +152,8 @@
.message-text .emoji {
position: relative;
top: -5px;
width: 3rem;
padding: .25rem
}