Browse Source
* First pass at chat user registration and validation * Disable chat if the user is disabled/blocked or the server hits max connections * Handle dropping sockets if chat is disabled * Fix origin in automated chat test * Work for updated chat moderation * Chat message markdown rendering and fix tests * Put /api/chat behind a chat user access token. Closes #1085 * Reject blocked username changes * More WIP moderation * Defer configuring chat until we know if it is enabled. Closes #1135 * chat user blocking. Closes #1096 * Add tests around user access for #1096 * Add external integration chat message API + update integration auth middleware to pass along integration name. Closes #1092 * Delete old chat messages from db as to not hold on to excessive data. Closes #1152 * Add schema migration for messages. Closes #1155 * Commit updated API documentation * Add chat load test * Shared db mutex and db optimizations * Simplify past display name handling * Use a new test db for each test run * Wire up the external messages actions + add tests for them * Move access tokens to be actual users * Run message pruning at launch + fix comparison * Do not return API users in disabled users response * Fix incorrect highlighting. Closes #1160 * Consolidate user table statements * Set the max process connection limit to 70% of maximum * Fix wrong old display name being returned in name change event * Delete the old chat server files * Wire back up the webhooks * Remove unused * Invalidate user cache on changes * Do not send rendered body as RawBody * Some cleanup * Standardize names for external API users to ExternalAPIUser * Do not log token * Checkout branch when building admin for testing * Bundle in dev admin for testing * Some cleanup * Cleanup js logs * Cleanup and standardize event names * Clean up some logging * Update API spec. Closes #1133 * Commit updated API documentation * Change paths to be better named * Commit updated API documentation * Update admin bundle * Fix duplicate event name * Rename scope var * Update admin bundle * Move connected clients controller into admin package * Fix collecting usernames for autocomplete purposes * No longer generate username when it is empty * Sort clients and users by timestamp * Move file to admin controller package * Swap, so the comments stay correct Co-authored-by: Jannik <jannik@outlook.com> * Use explicit type alias Co-authored-by: Jannik <jannik@outlook.com> * Remove commented code. Co-authored-by: Jannik <jannik@outlook.com> * Cleanup test * Remove some extra logging * Add some clarity * Update dev instance of admin for testing * Consolidate lines Co-authored-by: Jannik <jannik@outlook.com> * Remove commented unused vars Co-authored-by: Jannik <jannik@outlook.com> * Until needed do not return IP address with client list * Fix typo of wrong var * Typo led to a bad test. Fix typo and fix test. * Guard against the socket reconnecting on error if previously set to shutdown * Do not log access tokens * Return success message on enable/disable user * Clean up some inactionable error messages. Sent ban message. Sort banned users. * fix styling for when chat is completely disabled * Unused * guard against nil clients * Update dev admin bundle * Do not unhide messages when unblocking user just to be safe. Send removal action from the controller * Add convinience function for getting active connections for a single user * Lock db on these mutations * Cleanup force disconnect using GetClientsForUser and capture client reference explicitly * No longer re-showing banned user messages for safety. Removing this test. * Remove no longer needed comment * Tweaks to forbidden username handling. - Standardize naming to not use "block" but "forbidden" instead. - Pass array over the wire instead of string. - Add API test - Fix default list incorrectly being appended to custom list. * Logging cleanup * Update dev admin bundle * Add an artificial delay in order to visually see message being hidden when testing * Remove the user cache as it is a premature optimization * When connected to chat let the user know their current user details to sync the username in the UI * On connected send current display name back to client. - Move name change out of chat component. - Add additional event type constants. * Fix broken workflow due to typo * Troubleshoot workflow * Bump htm from 3.0.4 to 3.1.0 in /build/javascript (#1181) * Bump htm from 3.0.4 to 3.1.0 in /build/javascript Bumps [htm](https://github.com/developit/htm) from 3.0.4 to 3.1.0. - [Release notes](https://github.com/developit/htm/releases) - [Commits](https://github.com/developit/htm/compare/3.0.4...3.1.0) --- updated-dependencies: - dependency-name: htm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Run npm run build and update libraries Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com> * Commit updated Javascript packages * Re-send current user info when a rejected name change takes place * All socket writes should be through the send chan and not directly * Seed the random generator * Add keys and indexes to users table * a util to generate consistent emoji markup * console clean up * mod tidy * Commit updated API documentation * Handle the max payload size of a socket message. - Only close socket if x2 greater than the max size. - Send the user a message if a message is too large. - Surface the max size in bytes in the config. * Update admin bundle * Force all events to be sent in their own socket message and do not concatinate in a single message * Update chat embed to register for access token * Use a different access token for embed chat * Update the chat message bubble background color to be bolder * add base tag to open links in new window, closes #1220 * Support text input of :emoji: in chat (#1190) * Initial implementation of emoji injection * fix bookkeeping with multiple emoji * make the emoji lookup case-insensitive * try another solution for Caretposition * add title to emojis minor refactoring * bind moji injection to InputKeyUp * simplify the code replace all found emojis * inject emoji if the modifer is released earlier * more efficient emoji tag search * use json emoji.emoji as url * use createEmojiMarkup() * move emojify() to chat.js * emojify on paste * cleanup emoji titles in paste * update inputText in InputKeyup * mark emoji titles with 2*zwnj this way paste cleanup will not interfere with text which include zwnj * emoji should not change the inputText * Do not show join messages when chat is offline. Closes #1224 - Show stream starting/ending messages in chat. - When stream starts show everyone the welcome message. * Force scrolling chat to bottom after history is populated regardless of scroll position. Closes https://github.com/owncast/owncast/issues/1222 * use maxSocketPayloadSize to calculate total bytes of message payload (#1221) * utilize maxSocketPayloadSize from config; update chatInput to calculate based on that value instead of text value; remove usage of inputText for counting * add a buffer to account for entire websocket payload for message char counting; trim nbsp;'s from ends of messages when calculating count Co-authored-by: Gabe Kangas <gabek@real-ity.com> Co-authored-by: Owncast <owncast@owncast.online> Co-authored-by: Jannik <jannik@outlook.com> Co-authored-by: Ginger Wong <omqmail@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Meisam <39205857+MFTabriz@users.noreply.github.com>pull/1227/head
88 changed files with 10643 additions and 2233 deletions
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/controllers" |
||||
"github.com/owncast/owncast/core/chat" |
||||
"github.com/owncast/owncast/core/user" |
||||
) |
||||
|
||||
// GetConnectedClients returns currently connected clients.
|
||||
func GetConnectedClients(w http.ResponseWriter, r *http.Request) { |
||||
clients := chat.GetClients() |
||||
w.Header().Set("Content-Type", "application/json") |
||||
|
||||
if err := json.NewEncoder(w).Encode(clients); err != nil { |
||||
controllers.InternalErrorHandler(w, err) |
||||
} |
||||
} |
||||
|
||||
// ExternalGetConnectedClients returns currently connected clients.
|
||||
func ExternalGetConnectedClients(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { |
||||
GetConnectedClients(w, r) |
||||
} |
@ -1,18 +0,0 @@
@@ -1,18 +0,0 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/core" |
||||
) |
||||
|
||||
// GetConnectedClients returns currently connected clients.
|
||||
func GetConnectedClients(w http.ResponseWriter, r *http.Request) { |
||||
clients := core.GetChatClients() |
||||
w.Header().Set("Content-Type", "application/json") |
||||
|
||||
if err := json.NewEncoder(w).Encode(clients); err != nil { |
||||
InternalErrorHandler(w, err) |
||||
} |
||||
} |
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"time" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
"golang.org/x/time/rate" |
||||
|
||||
"github.com/gorilla/websocket" |
||||
"github.com/owncast/owncast/config" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/user" |
||||
"github.com/owncast/owncast/geoip" |
||||
) |
||||
|
||||
type ChatClient struct { |
||||
id uint |
||||
accessToken string |
||||
conn *websocket.Conn |
||||
User *user.User `json:"user"` |
||||
server *ChatServer |
||||
ipAddress string `json:"-"` |
||||
// Buffered channel of outbound messages.
|
||||
send chan []byte |
||||
rateLimiter *rate.Limiter |
||||
Geo *geoip.GeoDetails `json:"geo"` |
||||
MessageCount int `json:"messageCount"` |
||||
UserAgent string `json:"userAgent"` |
||||
ConnectedAt time.Time `json:"connectedAt"` |
||||
} |
||||
|
||||
type chatClientEvent struct { |
||||
data []byte |
||||
client *ChatClient |
||||
} |
||||
|
||||
const ( |
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second |
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second |
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10 |
||||
|
||||
// Maximum message size allowed from peer.
|
||||
// Larger messages get thrown away.
|
||||
// Messages > *2 the socket gets closed.
|
||||
maxMessageSize = config.MaxSocketPayloadSize |
||||
) |
||||
|
||||
var upgrader = websocket.Upgrader{ |
||||
ReadBufferSize: 1024, |
||||
WriteBufferSize: 1024, |
||||
} |
||||
|
||||
var ( |
||||
newline = []byte{'\n'} |
||||
space = []byte{' '} |
||||
) |
||||
|
||||
func (c *ChatClient) sendConnectedClientInfo() { |
||||
payload := events.EventPayload{ |
||||
"type": events.ConnectedUserInfo, |
||||
"user": c.User, |
||||
} |
||||
|
||||
c.sendPayload(payload) |
||||
} |
||||
|
||||
func (c *ChatClient) readPump() { |
||||
c.rateLimiter = rate.NewLimiter(0.6, 5) |
||||
|
||||
defer func() { |
||||
c.close() |
||||
}() |
||||
|
||||
// If somebody is sending 2x the max message size they're likely a bad actor
|
||||
// and should be disconnected. Below we throw away messages > max size.
|
||||
c.conn.SetReadLimit(maxMessageSize * 2) |
||||
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait)) |
||||
c.conn.SetPongHandler(func(string) error { _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) |
||||
for { |
||||
_, message, err := c.conn.ReadMessage() |
||||
|
||||
if err != nil { |
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { |
||||
c.close() |
||||
} |
||||
break |
||||
} |
||||
|
||||
// Throw away messages greater than max message size.
|
||||
if len(message) > maxMessageSize { |
||||
c.sendAction("Sorry, that message exceeded the maximum size and can't be delivered.") |
||||
continue |
||||
} |
||||
|
||||
// Guard against floods.
|
||||
if !c.passesRateLimit() { |
||||
continue |
||||
} |
||||
|
||||
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) |
||||
c.handleEvent(message) |
||||
} |
||||
} |
||||
|
||||
func (c *ChatClient) writePump() { |
||||
ticker := time.NewTicker(pingPeriod) |
||||
defer func() { |
||||
ticker.Stop() |
||||
c.conn.Close() |
||||
}() |
||||
|
||||
for { |
||||
select { |
||||
case message, ok := <-c.send: |
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) |
||||
if !ok { |
||||
// The server closed the channel.
|
||||
_ = c.conn.WriteMessage(websocket.CloseMessage, []byte{}) |
||||
return |
||||
} |
||||
|
||||
w, err := c.conn.NextWriter(websocket.TextMessage) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if _, err := w.Write(message); err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
|
||||
if err := w.Close(); err != nil { |
||||
return |
||||
} |
||||
case <-ticker.C: |
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) |
||||
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { |
||||
return |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *ChatClient) handleEvent(data []byte) { |
||||
c.server.inbound <- chatClientEvent{data: data, client: c} |
||||
} |
||||
|
||||
func (c *ChatClient) close() { |
||||
log.Traceln("client closed:", c.User.DisplayName, c.id, c.ipAddress) |
||||
|
||||
c.conn.Close() |
||||
c.server.unregister <- c |
||||
} |
||||
|
||||
func (c *ChatClient) passesRateLimit() bool { |
||||
if !c.rateLimiter.Allow() { |
||||
log.Debugln("Client", c.id, c.User.DisplayName, "has exceeded the messaging rate limiting thresholds.") |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
func (c *ChatClient) sendPayload(payload events.EventPayload) { |
||||
var data []byte |
||||
data, err := json.Marshal(payload) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return |
||||
} |
||||
|
||||
c.send <- data |
||||
} |
||||
|
||||
func (c *ChatClient) sendAction(message string) { |
||||
clientMessage := events.ActionEvent{ |
||||
MessageEvent: events.MessageEvent{ |
||||
Body: message, |
||||
}, |
||||
} |
||||
clientMessage.SetDefaults() |
||||
clientMessage.RenderBody() |
||||
c.sendPayload(clientMessage.GetBroadcastPayload()) |
||||
} |
@ -1,241 +0,0 @@
@@ -1,241 +0,0 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"time" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
"golang.org/x/net/websocket" |
||||
|
||||
"github.com/owncast/owncast/geoip" |
||||
"github.com/owncast/owncast/models" |
||||
"github.com/owncast/owncast/utils" |
||||
|
||||
"github.com/teris-io/shortid" |
||||
"golang.org/x/time/rate" |
||||
) |
||||
|
||||
const channelBufSize = 100 |
||||
|
||||
//Client represents a chat client.
|
||||
type Client struct { |
||||
ConnectedAt time.Time |
||||
MessageCount int |
||||
UserAgent string |
||||
IPAddress string |
||||
Username *string |
||||
ClientID string // How we identify unique viewers when counting viewer counts.
|
||||
Geo *geoip.GeoDetails `json:"geo"` |
||||
Ignore bool // If set to true this will not be treated as a viewer
|
||||
|
||||
socketID string // How we identify a single websocket client.
|
||||
ws *websocket.Conn |
||||
ch chan models.ChatEvent |
||||
pingch chan models.PingMessage |
||||
usernameChangeChannel chan models.NameChangeEvent |
||||
userJoinedChannel chan models.UserJoinedEvent |
||||
|
||||
doneCh chan bool |
||||
|
||||
rateLimiter *rate.Limiter |
||||
} |
||||
|
||||
// NewClient creates a new chat client.
|
||||
func NewClient(ws *websocket.Conn) *Client { |
||||
if ws == nil { |
||||
log.Panicln("ws cannot be nil") |
||||
} |
||||
|
||||
var ignoreClient = false |
||||
for _, extraData := range ws.Config().Protocol { |
||||
if extraData == "IGNORE_CLIENT" { |
||||
ignoreClient = true |
||||
} |
||||
} |
||||
|
||||
ch := make(chan models.ChatEvent, channelBufSize) |
||||
doneCh := make(chan bool) |
||||
pingch := make(chan models.PingMessage) |
||||
usernameChangeChannel := make(chan models.NameChangeEvent) |
||||
userJoinedChannel := make(chan models.UserJoinedEvent) |
||||
|
||||
ipAddress := utils.GetIPAddressFromRequest(ws.Request()) |
||||
userAgent := ws.Request().UserAgent() |
||||
socketID, _ := shortid.Generate() |
||||
clientID := socketID |
||||
|
||||
rateLimiter := rate.NewLimiter(0.6, 5) |
||||
|
||||
return &Client{time.Now(), 0, userAgent, ipAddress, nil, clientID, nil, ignoreClient, socketID, ws, ch, pingch, usernameChangeChannel, userJoinedChannel, doneCh, rateLimiter} |
||||
} |
||||
|
||||
func (c *Client) write(msg models.ChatEvent) { |
||||
select { |
||||
case c.ch <- msg: |
||||
default: |
||||
_server.removeClient(c) |
||||
_server.err(fmt.Errorf("client %s is disconnected", c.ClientID)) |
||||
} |
||||
} |
||||
|
||||
// Listen Write and Read request via channel.
|
||||
func (c *Client) listen() { |
||||
go c.listenWrite() |
||||
c.listenRead() |
||||
} |
||||
|
||||
// Listen write request via channel.
|
||||
func (c *Client) listenWrite() { |
||||
for { |
||||
select { |
||||
// Send a PING keepalive
|
||||
case msg := <-c.pingch: |
||||
if err := websocket.JSON.Send(c.ws, msg); err != nil { |
||||
c.handleClientSocketError(err) |
||||
} |
||||
// send message to the client
|
||||
case msg := <-c.ch: |
||||
if err := websocket.JSON.Send(c.ws, msg); err != nil { |
||||
c.handleClientSocketError(err) |
||||
} |
||||
case msg := <-c.usernameChangeChannel: |
||||
if err := websocket.JSON.Send(c.ws, msg); err != nil { |
||||
c.handleClientSocketError(err) |
||||
} |
||||
case msg := <-c.userJoinedChannel: |
||||
if err := websocket.JSON.Send(c.ws, msg); err != nil { |
||||
c.handleClientSocketError(err) |
||||
} |
||||
|
||||
// receive done request
|
||||
case <-c.doneCh: |
||||
_server.removeClient(c) |
||||
c.doneCh <- true // for listenRead method
|
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *Client) handleClientSocketError(err error) { |
||||
_server.removeClient(c) |
||||
} |
||||
|
||||
func (c *Client) passesRateLimit() bool { |
||||
if !c.rateLimiter.Allow() { |
||||
log.Debugln("Client", c.ClientID, "has exceeded the messaging rate limiting thresholds.") |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
// Listen read request via channel.
|
||||
func (c *Client) listenRead() { |
||||
for { |
||||
select { |
||||
// receive done request
|
||||
case <-c.doneCh: |
||||
_server.remove(c) |
||||
c.doneCh <- true // for listenWrite method
|
||||
return |
||||
|
||||
// read data from websocket connection
|
||||
default: |
||||
var data []byte |
||||
if err := websocket.Message.Receive(c.ws, &data); err != nil { |
||||
if err == io.EOF { |
||||
c.doneCh <- true |
||||
return |
||||
} |
||||
c.handleClientSocketError(err) |
||||
} |
||||
|
||||
if !c.passesRateLimit() { |
||||
continue |
||||
} |
||||
|
||||
var messageTypeCheck map[string]interface{} |
||||
|
||||
// Bad messages should be thrown away
|
||||
if err := json.Unmarshal(data, &messageTypeCheck); err != nil { |
||||
log.Debugln("Badly formatted message received from", c.Username, c.ws.Request().RemoteAddr) |
||||
continue |
||||
} |
||||
|
||||
// If we can't tell the type of message, also throw it away.
|
||||
if messageTypeCheck == nil { |
||||
log.Debugln("Untyped message received from", c.Username, c.ws.Request().RemoteAddr) |
||||
continue |
||||
} |
||||
|
||||
messageType := messageTypeCheck["type"].(string) |
||||
|
||||
if messageType == models.MessageSent { |
||||
c.chatMessageReceived(data) |
||||
} else if messageType == models.UserNameChanged { |
||||
c.userChangedName(data) |
||||
} else if messageType == models.UserJoined { |
||||
c.userJoined(data) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *Client) userJoined(data []byte) { |
||||
var msg models.UserJoinedEvent |
||||
if err := json.Unmarshal(data, &msg); err != nil { |
||||
log.Errorln(err) |
||||
return |
||||
} |
||||
|
||||
msg.ID = shortid.MustGenerate() |
||||
msg.Type = models.UserJoined |
||||
msg.Timestamp = time.Now() |
||||
|
||||
c.Username = &msg.Username |
||||
|
||||
_server.userJoined(msg) |
||||
} |
||||
|
||||
func (c *Client) userChangedName(data []byte) { |
||||
var msg models.NameChangeEvent |
||||
if err := json.Unmarshal(data, &msg); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
msg.Type = models.UserNameChanged |
||||
msg.ID = shortid.MustGenerate() |
||||
_server.usernameChanged(msg) |
||||
c.Username = &msg.NewName |
||||
} |
||||
|
||||
func (c *Client) chatMessageReceived(data []byte) { |
||||
var msg models.ChatEvent |
||||
if err := json.Unmarshal(data, &msg); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
|
||||
msg.SetDefaults() |
||||
|
||||
c.MessageCount++ |
||||
c.Username = &msg.Author |
||||
|
||||
msg.ClientID = c.ClientID |
||||
msg.RenderAndSanitizeMessageBody() |
||||
|
||||
_server.SendToAll(msg) |
||||
} |
||||
|
||||
// GetViewerClientFromChatClient returns a general models.Client from a chat websocket client.
|
||||
func (c *Client) GetViewerClientFromChatClient() models.Client { |
||||
return models.Client{ |
||||
ConnectedAt: c.ConnectedAt, |
||||
MessageCount: c.MessageCount, |
||||
UserAgent: c.UserAgent, |
||||
IPAddress: c.IPAddress, |
||||
Username: c.Username, |
||||
ClientID: c.ClientID, |
||||
Geo: geoip.GetGeoFromIP(c.IPAddress), |
||||
} |
||||
} |
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/core/user" |
||||
"github.com/owncast/owncast/core/webhooks" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func (s *ChatServer) userNameChanged(eventData chatClientEvent) { |
||||
var receivedEvent events.NameChangeEvent |
||||
if err := json.Unmarshal(eventData.data, &receivedEvent); err != nil { |
||||
log.Errorln("error unmarshalling to NameChangeEvent", err) |
||||
return |
||||
} |
||||
|
||||
proposedUsername := receivedEvent.NewName |
||||
blocklist := data.GetForbiddenUsernameList() |
||||
|
||||
for _, blockedName := range blocklist { |
||||
normalizedName := strings.TrimSpace(blockedName) |
||||
normalizedName = strings.ToLower(normalizedName) |
||||
if strings.Contains(normalizedName, proposedUsername) { |
||||
// Denied.
|
||||
log.Debugln(eventData.client.User.DisplayName, "blocked from changing name to", proposedUsername, "due to blocked name", normalizedName) |
||||
message := fmt.Sprintf("You cannot change your name to **%s**.", proposedUsername) |
||||
s.sendActionToClient(eventData.client, message) |
||||
|
||||
// Resend the client's user so their username is in sync.
|
||||
eventData.client.sendConnectedClientInfo() |
||||
|
||||
return |
||||
} |
||||
} |
||||
|
||||
savedUser := user.GetUserByToken(eventData.client.accessToken) |
||||
oldName := savedUser.DisplayName |
||||
|
||||
// Save the new name
|
||||
user.ChangeUsername(eventData.client.User.Id, receivedEvent.NewName) |
||||
|
||||
// Update the connected clients associated user with the new name
|
||||
eventData.client.User = savedUser |
||||
|
||||
// Send chat event letting everyone about about the name change
|
||||
savedUser.DisplayName = receivedEvent.NewName |
||||
|
||||
broadcastEvent := events.NameChangeBroadcast{ |
||||
Oldname: oldName, |
||||
} |
||||
broadcastEvent.User = savedUser |
||||
broadcastEvent.SetDefaults() |
||||
payload := broadcastEvent.GetBroadcastPayload() |
||||
if err := s.Broadcast(payload); err != nil { |
||||
log.Errorln("error broadcasting NameChangeEvent", err) |
||||
return |
||||
} |
||||
|
||||
// Send chat user name changed webhook
|
||||
receivedEvent.User = savedUser |
||||
webhooks.SendChatEventUsernameChanged(receivedEvent) |
||||
} |
||||
|
||||
func (s *ChatServer) userMessageSent(eventData chatClientEvent) { |
||||
var event events.UserMessageEvent |
||||
if err := json.Unmarshal(eventData.data, &event); err != nil { |
||||
log.Errorln("error unmarshalling to UserMessageEvent", err) |
||||
return |
||||
} |
||||
|
||||
event.SetDefaults() |
||||
|
||||
// Ignore empty messages
|
||||
if event.Empty() { |
||||
return |
||||
} |
||||
|
||||
event.User = user.GetUserByToken(eventData.client.accessToken) |
||||
|
||||
// Guard against nil users
|
||||
if event.User == nil { |
||||
return |
||||
} |
||||
|
||||
payload := event.GetBroadcastPayload() |
||||
if err := s.Broadcast(payload); err != nil { |
||||
log.Errorln("error broadcasting UserMessageEvent payload", err) |
||||
return |
||||
} |
||||
|
||||
// Send chat message sent webhook
|
||||
webhooks.SendChatEvent(&event) |
||||
|
||||
SaveUserMessage(event) |
||||
|
||||
eventData.client.MessageCount = eventData.client.MessageCount + 1 |
||||
} |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
package events |
||||
|
||||
type ActionEvent struct { |
||||
Event |
||||
MessageEvent |
||||
} |
||||
|
||||
// ActionEvent will return the object to send to all chat users.
|
||||
func (e *ActionEvent) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"id": e.Id, |
||||
"timestamp": e.Timestamp, |
||||
"body": e.Body, |
||||
"type": e.GetMessageType(), |
||||
} |
||||
} |
||||
|
||||
func (e *ActionEvent) GetMessageType() EventType { |
||||
return ChatActionSent |
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package events |
||||
|
||||
// EventType is the type of a websocket event.
|
||||
type EventType = string |
||||
|
||||
const ( |
||||
// MessageSent is the event sent when a chat event takes place.
|
||||
MessageSent EventType = "CHAT" |
||||
// UserJoined is the event sent when a chat user join action takes place.
|
||||
UserJoined EventType = "USER_JOINED" |
||||
// UserNameChanged is the event sent when a chat username change takes place.
|
||||
UserNameChanged EventType = "NAME_CHANGE" |
||||
// VisibiltyToggled is the event sent when a chat message's visibility changes.
|
||||
VisibiltyToggled EventType = "VISIBILITY-UPDATE" |
||||
// PING is a ping message.
|
||||
PING EventType = "PING" |
||||
// PONG is a pong message.
|
||||
PONG EventType = "PONG" |
||||
// StreamStarted represents a stream started event.
|
||||
StreamStarted EventType = "STREAM_STARTED" |
||||
// StreamStopped represents a stream stopped event.
|
||||
StreamStopped EventType = "STREAM_STOPPED" |
||||
// SystemMessageSent is the event sent when a system message is sent.
|
||||
SystemMessageSent EventType = "SYSTEM" |
||||
// ChatDisabled is when a user is explicitly disabled and blocked from using chat.
|
||||
ChatDisabled EventType = "CHAT_DISABLED" |
||||
// ConnectedUserInfo is a private event to a user letting them know their user details.
|
||||
ConnectedUserInfo EventType = "CONNECTED_USER_INFO" |
||||
// ChatActionSent is a generic chat action that can be used for anything that doesn't need specific handling or formatting.
|
||||
ChatActionSent EventType = "CHAT_ACTION" |
||||
ErrorNeedsRegistration EventType = "ERROR_NEEDS_REGISTRATION" |
||||
ErrorMaxConnectionsExceeded EventType = "ERROR_MAX_CONNECTIONS_EXCEEDED" |
||||
ErrorUserDisabled EventType = "ERROR_USER_DISABLED" |
||||
) |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package events |
||||
|
||||
// NameChangeEvent is received when a user changes their chat display name.
|
||||
type NameChangeEvent struct { |
||||
Event |
||||
UserEvent |
||||
NewName string `json:"newName"` |
||||
} |
||||
|
||||
// NameChangeEventBroadcast is fired when a user changes their chat display name.
|
||||
type NameChangeBroadcast struct { |
||||
Event |
||||
UserEvent |
||||
Oldname string `json:"oldName"` |
||||
} |
||||
|
||||
// GetBroadcastPayload will return the object to send to all chat users.
|
||||
func (e *NameChangeBroadcast) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"id": e.Id, |
||||
"timestamp": e.Timestamp, |
||||
"user": e.User, |
||||
"oldName": e.Oldname, |
||||
"type": UserNameChanged, |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package events |
||||
|
||||
import "github.com/owncast/owncast/core/data" |
||||
|
||||
// SystemMessageEvent is a message displayed in chat on behalf of the server.
|
||||
type SystemMessageEvent struct { |
||||
Event |
||||
MessageEvent |
||||
} |
||||
|
||||
// SystemMessageEvent will return the object to send to all chat users.
|
||||
func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"id": e.Id, |
||||
"timestamp": e.Timestamp, |
||||
"body": e.Body, |
||||
"type": SystemMessageSent, |
||||
"user": EventPayload{ |
||||
"displayName": data.GetServerName(), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func (e *SystemMessageEvent) GetMessageType() EventType { |
||||
return SystemMessageSent |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
package events |
||||
|
||||
// UserDisabledEvent is the event fired when a user is banned/blocked and disconnected from chat.
|
||||
type UserDisabledEvent struct { |
||||
Event |
||||
UserEvent |
||||
} |
||||
|
||||
// GetBroadcastPayload will return the object to send to all chat users.
|
||||
func (e *UserDisabledEvent) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"type": ErrorUserDisabled, |
||||
"id": e.Id, |
||||
"timestamp": e.Timestamp, |
||||
"user": e.User, |
||||
} |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
package events |
||||
|
||||
// UserJoinedEvent is the event fired when a user joins chat.
|
||||
type UserJoinedEvent struct { |
||||
Event |
||||
UserEvent |
||||
} |
||||
|
||||
// GetBroadcastPayload will return the object to send to all chat users.
|
||||
func (e *UserJoinedEvent) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"type": UserJoined, |
||||
"id": e.Id, |
||||
"timestamp": e.Timestamp, |
||||
"user": e.User, |
||||
} |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
package events |
||||
|
||||
// UserMessageEvent is an inbound message from a user.
|
||||
type UserMessageEvent struct { |
||||
Event |
||||
UserEvent |
||||
MessageEvent |
||||
} |
||||
|
||||
// GetBroadcastPayload will return the object to send to all chat users.
|
||||
func (e *UserMessageEvent) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"id": e.Id, |
||||
"timestamp": e.Timestamp, |
||||
"body": e.Body, |
||||
"user": e.User, |
||||
"type": MessageSent, |
||||
"visible": e.HiddenAt == nil, |
||||
} |
||||
} |
||||
|
||||
func (e *UserMessageEvent) GetMessageType() EventType { |
||||
return MessageSent |
||||
} |
@ -1,171 +1,309 @@
@@ -1,171 +1,309 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
|
||||
_ "github.com/mattn/go-sqlite3" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/core/user" |
||||
"github.com/owncast/owncast/models" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
var _db *sql.DB |
||||
var _datastore *data.Datastore |
||||
|
||||
const ( |
||||
maxBacklogHours = 5 // Keep backlog max hours worth of messages
|
||||
maxBacklogNumber = 50 // Return max number of messages in history request
|
||||
) |
||||
|
||||
func setupPersistence() { |
||||
_db = data.GetDatabase() |
||||
createTable() |
||||
_datastore = data.GetDatastore() |
||||
createMessagesTable() |
||||
|
||||
chatDataPruner := time.NewTicker(5 * time.Minute) |
||||
go func() { |
||||
runPruner() |
||||
for range chatDataPruner.C { |
||||
runPruner() |
||||
} |
||||
}() |
||||
} |
||||
|
||||
func createTable() { |
||||
func createMessagesTable() { |
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS messages ( |
||||
"id" string NOT NULL PRIMARY KEY, |
||||
"author" TEXT, |
||||
"user_id" INTEGER, |
||||
"body" TEXT, |
||||
"messageType" TEXT, |
||||
"visible" INTEGER, |
||||
"timestamp" DATE |
||||
"eventType" TEXT, |
||||
"hidden_at" DATETIME, |
||||
"timestamp" DATETIME |
||||
);` |
||||
|
||||
stmt, err := _db.Prepare(createTableSQL) |
||||
stmt, err := _datastore.DB.Prepare(createTableSQL) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
log.Fatal("error creating chat messages table", err) |
||||
} |
||||
defer stmt.Close() |
||||
if _, err := stmt.Exec(); err != nil { |
||||
log.Warnln(err) |
||||
log.Fatal("error creating chat messages table", err) |
||||
} |
||||
} |
||||
|
||||
func addMessage(message models.ChatEvent) { |
||||
tx, err := _db.Begin() |
||||
func SaveUserMessage(event events.UserMessageEvent) { |
||||
saveEvent(event.Id, event.User.Id, event.Body, event.Type, event.HiddenAt, event.Timestamp) |
||||
} |
||||
|
||||
func saveEvent(id string, userId string, body string, eventType string, hidden *time.Time, timestamp time.Time) { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
log.Errorln("error saving", eventType, err) |
||||
return |
||||
} |
||||
stmt, err := tx.Prepare("INSERT INTO messages(id, author, body, messageType, visible, timestamp) values(?, ?, ?, ?, ?, ?)") |
||||
|
||||
defer tx.Rollback() // nolint
|
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO messages(id, user_id, body, eventType, hidden_at, timestamp) values(?, ?, ?, ?, ?, ?)") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
log.Errorln("error saving", eventType, err) |
||||
return |
||||
} |
||||
|
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(message.ID, message.Author, message.Body, message.MessageType, 1, message.Timestamp); err != nil { |
||||
log.Fatal(err) |
||||
if _, err = stmt.Exec(id, userId, body, eventType, hidden, timestamp); err != nil { |
||||
log.Errorln("error saving", eventType, err) |
||||
return |
||||
} |
||||
if err := tx.Commit(); err != nil { |
||||
log.Fatal(err) |
||||
if err = tx.Commit(); err != nil { |
||||
log.Errorln("error saving", eventType, err) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func getChat(query string) []models.ChatEvent { |
||||
history := make([]models.ChatEvent, 0) |
||||
rows, err := _db.Query(query) |
||||
func getChat(query string) []events.UserMessageEvent { |
||||
history := make([]events.UserMessageEvent, 0) |
||||
rows, err := _datastore.DB.Query(query) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
log.Errorln("error fetching chat history", err) |
||||
return history |
||||
} |
||||
defer rows.Close() |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var author string |
||||
var userId string |
||||
var body string |
||||
var messageType models.EventType |
||||
var visible int |
||||
var hiddenAt *time.Time |
||||
var timestamp time.Time |
||||
|
||||
err = rows.Scan(&id, &author, &body, &messageType, &visible, ×tamp) |
||||
var userDisplayName *string |
||||
var userDisplayColor *int |
||||
var userCreatedAt *time.Time |
||||
var userDisabledAt *time.Time |
||||
var previousUsernames *string |
||||
var userNameChangedAt *time.Time |
||||
|
||||
// Convert a database row into a chat event
|
||||
err = rows.Scan(&id, &userId, &body, &messageType, &hiddenAt, ×tamp, &userDisplayName, &userDisplayColor, &userCreatedAt, &userDisabledAt, &previousUsernames, &userNameChangedAt) |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
log.Error("There is a problem with the chat database. Restore a backup of owncast.db or remove it and start over.") |
||||
log.Errorln("There is a problem converting query to chat objects. Please report this:", query) |
||||
break |
||||
} |
||||
|
||||
message := models.ChatEvent{} |
||||
message.ID = id |
||||
message.Author = author |
||||
message.Body = body |
||||
message.MessageType = messageType |
||||
message.Visible = visible == 1 |
||||
message.Timestamp = timestamp |
||||
// System messages and chat actions are special and are not from real users
|
||||
if messageType == events.SystemMessageSent || messageType == events.ChatActionSent { |
||||
name := "Owncast" |
||||
userDisplayName = &name |
||||
color := 200 |
||||
userDisplayColor = &color |
||||
} |
||||
|
||||
history = append(history, message) |
||||
} |
||||
if previousUsernames == nil { |
||||
previousUsernames = userDisplayName |
||||
} |
||||
|
||||
if userCreatedAt == nil { |
||||
now := time.Now() |
||||
userCreatedAt = &now |
||||
} |
||||
|
||||
user := user.User{ |
||||
Id: userId, |
||||
AccessToken: "", |
||||
DisplayName: *userDisplayName, |
||||
DisplayColor: *userDisplayColor, |
||||
CreatedAt: *userCreatedAt, |
||||
DisabledAt: userDisabledAt, |
||||
NameChangedAt: userNameChangedAt, |
||||
PreviousNames: strings.Split(*previousUsernames, ","), |
||||
} |
||||
|
||||
if err := rows.Err(); err != nil { |
||||
log.Fatal(err) |
||||
message := events.UserMessageEvent{ |
||||
Event: events.Event{ |
||||
Type: messageType, |
||||
Id: id, |
||||
Timestamp: timestamp, |
||||
}, |
||||
UserEvent: events.UserEvent{ |
||||
User: &user, |
||||
HiddenAt: hiddenAt, |
||||
}, |
||||
MessageEvent: events.MessageEvent{ |
||||
Body: body, |
||||
RawBody: body, |
||||
}, |
||||
} |
||||
|
||||
history = append(history, message) |
||||
} |
||||
|
||||
return history |
||||
} |
||||
|
||||
func getChatModerationHistory() []models.ChatEvent { |
||||
var query = "SELECT * FROM messages WHERE messageType == 'CHAT' AND datetime(timestamp) >=datetime('now', '-5 Hour')" |
||||
func GetChatModerationHistory() []events.UserMessageEvent { |
||||
// Get all messages regardless of visibility
|
||||
var query = "SELECT messages.id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC" |
||||
return getChat(query) |
||||
} |
||||
|
||||
func getChatHistory() []models.ChatEvent { |
||||
// Get all messages sent within the past 5hrs, max 50
|
||||
var query = "SELECT * FROM (SELECT * FROM messages WHERE datetime(timestamp) >=datetime('now', '-5 Hour') AND visible = 1 ORDER BY timestamp DESC LIMIT 50) ORDER BY timestamp asc" |
||||
func GetChatHistory() []events.UserMessageEvent { |
||||
// Get all visible messages
|
||||
var query = fmt.Sprintf("SELECT id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM (SELECT * FROM messages LEFT OUTER JOIN users ON messages.user_id = users.id WHERE hidden_at IS NULL ORDER BY timestamp DESC LIMIT %d) ORDER BY timestamp asc", maxBacklogNumber) |
||||
return getChat(query) |
||||
} |
||||
|
||||
// SetMessageVisibilityForUserId will bulk change the visibility of messages for a user
|
||||
// and then send out visibility changed events to chat clients.
|
||||
func SetMessageVisibilityForUserId(userID string, visible bool) error { |
||||
// Get a list of IDs from this user within the 5hr window to send to the connected clients to hide
|
||||
ids := make([]string, 0) |
||||
query := fmt.Sprintf("SELECT messages.id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS '%s'", userID) |
||||
messages := getChat(query) |
||||
|
||||
if len(messages) == 0 { |
||||
return nil |
||||
} |
||||
|
||||
for _, message := range messages { |
||||
ids = append(ids, message.Id) |
||||
} |
||||
|
||||
// Tell the clients to hide/show these messages.
|
||||
return SetMessagesVisibility(ids, visible) |
||||
} |
||||
|
||||
func saveMessageVisibility(messageIDs []string, visible bool) error { |
||||
tx, err := _db.Begin() |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
return err |
||||
} |
||||
|
||||
stmt, err := tx.Prepare("UPDATE messages SET visible=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")") |
||||
stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")") |
||||
|
||||
if err != nil { |
||||
log.Fatal(err) |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
var hiddenAt *time.Time |
||||
if !visible { |
||||
now := time.Now() |
||||
hiddenAt = &now |
||||
} else { |
||||
hiddenAt = nil |
||||
} |
||||
|
||||
args := make([]interface{}, len(messageIDs)+1) |
||||
args[0] = visible |
||||
args[0] = hiddenAt |
||||
for i, id := range messageIDs { |
||||
args[i+1] = id |
||||
} |
||||
|
||||
if _, err := stmt.Exec(args...); err != nil { |
||||
log.Fatal(err) |
||||
if _, err = stmt.Exec(args...); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := tx.Commit(); err != nil { |
||||
log.Fatal(err) |
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func getMessageById(messageID string) (models.ChatEvent, error) { |
||||
func getMessageById(messageID string) (*events.UserMessageEvent, error) { |
||||
var query = "SELECT * FROM messages WHERE id = ?" |
||||
row := _db.QueryRow(query, messageID) |
||||
row := _datastore.DB.QueryRow(query, messageID) |
||||
|
||||
var id string |
||||
var author string |
||||
var userId string |
||||
var body string |
||||
var messageType models.EventType |
||||
var visible int |
||||
var eventType models.EventType |
||||
var hiddenAt *time.Time |
||||
var timestamp time.Time |
||||
|
||||
err := row.Scan(&id, &author, &body, &messageType, &visible, ×tamp) |
||||
err := row.Scan(&id, &userId, &body, &eventType, &hiddenAt, ×tamp) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return models.ChatEvent{}, err |
||||
return nil, err |
||||
} |
||||
|
||||
return models.ChatEvent{ |
||||
ID: id, |
||||
Author: author, |
||||
Body: body, |
||||
MessageType: messageType, |
||||
Visible: visible == 1, |
||||
Timestamp: timestamp, |
||||
user := user.GetUserById(userId) |
||||
|
||||
return &events.UserMessageEvent{ |
||||
events.Event{ |
||||
Type: eventType, |
||||
Id: id, |
||||
Timestamp: timestamp, |
||||
}, |
||||
events.UserEvent{ |
||||
User: user, |
||||
HiddenAt: hiddenAt, |
||||
}, |
||||
events.MessageEvent{ |
||||
Body: body, |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
// Only keep recent messages so we don't keep more chat data than needed
|
||||
// for privacy and efficiency reasons.
|
||||
func runPruner() { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
log.Traceln("Removing chat messages older than", maxBacklogHours, "hours") |
||||
|
||||
deleteStatement := `DELETE FROM messages WHERE timestamp <= datetime('now', 'localtime', ?)` |
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
|
||||
stmt, err := tx.Prepare(deleteStatement) |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err = stmt.Exec(fmt.Sprintf("-%d hours", maxBacklogHours)); err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
if err = tx.Commit(); err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
} |
||||
|
@ -1,191 +1,317 @@
@@ -1,191 +1,317 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"fmt" |
||||
"encoding/json" |
||||
"net/http" |
||||
"sync" |
||||
"time" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
"golang.org/x/net/websocket" |
||||
|
||||
"github.com/gorilla/websocket" |
||||
|
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/core/user" |
||||
"github.com/owncast/owncast/core/webhooks" |
||||
"github.com/owncast/owncast/models" |
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
var ( |
||||
_server *server |
||||
) |
||||
var _server *ChatServer |
||||
|
||||
var l = &sync.RWMutex{} |
||||
type ChatServer struct { |
||||
mu sync.RWMutex |
||||
seq uint |
||||
clients map[uint]*ChatClient |
||||
maxClientCount uint |
||||
|
||||
// Server represents the server which handles the chat.
|
||||
type server struct { |
||||
Clients map[string]*Client |
||||
// send outbound message payload to all clients
|
||||
outbound chan []byte |
||||
|
||||
pattern string |
||||
listener models.ChatListener |
||||
// receive inbound message payload from all clients
|
||||
inbound chan chatClientEvent |
||||
|
||||
addCh chan *Client |
||||
delCh chan *Client |
||||
sendAllCh chan models.ChatEvent |
||||
pingCh chan models.PingMessage |
||||
doneCh chan bool |
||||
errCh chan error |
||||
// unregister requests from clients.
|
||||
unregister chan *ChatClient |
||||
} |
||||
|
||||
// Add adds a client to the server.
|
||||
func (s *server) add(c *Client) { |
||||
s.addCh <- c |
||||
} |
||||
func NewChat() *ChatServer { |
||||
server := &ChatServer{ |
||||
clients: map[uint]*ChatClient{}, |
||||
outbound: make(chan []byte), |
||||
inbound: make(chan chatClientEvent), |
||||
unregister: make(chan *ChatClient), |
||||
maxClientCount: handleMaxConnectionCount(), |
||||
} |
||||
|
||||
// Remove removes a client from the server.
|
||||
func (s *server) remove(c *Client) { |
||||
s.delCh <- c |
||||
return server |
||||
} |
||||
|
||||
// SendToAll sends a message to all of the connected clients.
|
||||
func (s *server) SendToAll(msg models.ChatEvent) { |
||||
s.sendAllCh <- msg |
||||
func (s *ChatServer) Run() { |
||||
for { |
||||
select { |
||||
case client := <-s.unregister: |
||||
if _, ok := s.clients[client.id]; ok { |
||||
s.mu.Lock() |
||||
delete(s.clients, client.id) |
||||
close(client.send) |
||||
s.mu.Unlock() |
||||
} |
||||
|
||||
case message := <-s.inbound: |
||||
s.eventReceived(message) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Err handles an error.
|
||||
func (s *server) err(err error) { |
||||
s.errCh <- err |
||||
// Addclient registers new connection as a User.
|
||||
func (s *ChatServer) Addclient(conn *websocket.Conn, user *user.User, accessToken string, userAgent string) *ChatClient { |
||||
client := &ChatClient{ |
||||
server: s, |
||||
conn: conn, |
||||
User: user, |
||||
ipAddress: conn.RemoteAddr().String(), |
||||
accessToken: accessToken, |
||||
send: make(chan []byte, 256), |
||||
UserAgent: userAgent, |
||||
ConnectedAt: time.Now(), |
||||
} |
||||
|
||||
s.mu.Lock() |
||||
{ |
||||
client.id = s.seq |
||||
s.clients[client.id] = client |
||||
s.seq++ |
||||
} |
||||
s.mu.Unlock() |
||||
|
||||
log.Traceln("Adding client", client.id, "total count:", len(s.clients)) |
||||
|
||||
go client.writePump() |
||||
go client.readPump() |
||||
|
||||
client.sendConnectedClientInfo() |
||||
|
||||
if getStatus().Online { |
||||
s.sendUserJoinedMessage(client) |
||||
s.sendWelcomeMessageToClient(client) |
||||
} |
||||
|
||||
return client |
||||
} |
||||
|
||||
func (s *server) sendAll(msg models.ChatEvent) { |
||||
l.RLock() |
||||
for _, c := range s.Clients { |
||||
c.write(msg) |
||||
func (s *ChatServer) sendUserJoinedMessage(c *ChatClient) { |
||||
userJoinedEvent := events.UserJoinedEvent{} |
||||
userJoinedEvent.SetDefaults() |
||||
userJoinedEvent.User = c.User |
||||
|
||||
if err := s.Broadcast(userJoinedEvent.GetBroadcastPayload()); err != nil { |
||||
log.Errorln("error adding client to chat server", err) |
||||
} |
||||
l.RUnlock() |
||||
|
||||
// Send chat user joined webhook
|
||||
webhooks.SendChatEventUserJoined(userJoinedEvent) |
||||
} |
||||
|
||||
func (s *server) ping() { |
||||
ping := models.PingMessage{MessageType: models.PING} |
||||
func (s *ChatServer) ClientClosed(c *ChatClient) { |
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
c.close() |
||||
|
||||
l.RLock() |
||||
for _, c := range s.Clients { |
||||
c.pingch <- ping |
||||
if _, ok := s.clients[c.id]; ok { |
||||
log.Debugln("Deleting", c.id) |
||||
delete(s.clients, c.id) |
||||
} |
||||
l.RUnlock() |
||||
} |
||||
|
||||
func (s *server) usernameChanged(msg models.NameChangeEvent) { |
||||
l.RLock() |
||||
for _, c := range s.Clients { |
||||
c.usernameChangeChannel <- msg |
||||
func (s *ChatServer) HandleClientConnection(w http.ResponseWriter, r *http.Request) { |
||||
if data.GetChatDisabled() { |
||||
_, _ = w.Write([]byte(events.ChatDisabled)) |
||||
return |
||||
} |
||||
l.RUnlock() |
||||
|
||||
go webhooks.SendChatEventUsernameChanged(msg) |
||||
} |
||||
// Limit concurrent chat connections
|
||||
if uint(len(s.clients)) >= s.maxClientCount { |
||||
log.Warnln("rejecting incoming client connection as it exceeds the max client count of", s.maxClientCount) |
||||
_, _ = w.Write([]byte(events.ErrorMaxConnectionsExceeded)) |
||||
return |
||||
} |
||||
|
||||
func (s *server) userJoined(msg models.UserJoinedEvent) { |
||||
l.RLock() |
||||
if s.listener.IsStreamConnected() { |
||||
for _, c := range s.Clients { |
||||
c.userJoinedChannel <- msg |
||||
} |
||||
conn, err := upgrader.Upgrade(w, r, nil) |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
|
||||
accessToken := r.URL.Query().Get("accessToken") |
||||
if accessToken == "" { |
||||
log.Errorln("Access token is required") |
||||
// Return HTTP status code
|
||||
conn.Close() |
||||
return |
||||
} |
||||
|
||||
// A user is required to use the websocket
|
||||
user := user.GetUserByToken(accessToken) |
||||
if user == nil { |
||||
_ = conn.WriteJSON(events.EventPayload{ |
||||
"type": events.ErrorNeedsRegistration, |
||||
}) |
||||
// Send error that registration is required
|
||||
conn.Close() |
||||
return |
||||
} |
||||
|
||||
// User is disabled therefore we should disconnect.
|
||||
if user.DisabledAt != nil { |
||||
log.Traceln("Disabled user", user.Id, user.DisplayName, "rejected") |
||||
_ = conn.WriteJSON(events.EventPayload{ |
||||
"type": events.ErrorUserDisabled, |
||||
}) |
||||
conn.Close() |
||||
return |
||||
} |
||||
l.RUnlock() |
||||
|
||||
go webhooks.SendChatEventUserJoined(msg) |
||||
userAgent := r.UserAgent() |
||||
|
||||
s.Addclient(conn, user, accessToken, userAgent) |
||||
} |
||||
|
||||
func (s *server) onConnection(ws *websocket.Conn) { |
||||
client := NewClient(ws) |
||||
// Broadcast sends message to all connected clients.
|
||||
func (s *ChatServer) Broadcast(payload events.EventPayload) error { |
||||
data, err := json.Marshal(payload) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.mu.Lock() |
||||
defer s.mu.Unlock() |
||||
|
||||
defer func() { |
||||
s.removeClient(client) |
||||
for _, client := range s.clients { |
||||
if client == nil { |
||||
continue |
||||
} |
||||
|
||||
if err := ws.Close(); err != nil { |
||||
log.Debugln(err) |
||||
//s.errCh <- err
|
||||
select { |
||||
case client.send <- data: |
||||
default: |
||||
close(client.send) |
||||
delete(s.clients, client.id) |
||||
} |
||||
}() |
||||
} |
||||
|
||||
s.add(client) |
||||
client.listen() |
||||
return nil |
||||
} |
||||
|
||||
// Listen and serve.
|
||||
// It serves client connection and broadcast request.
|
||||
func (s *server) Listen() { |
||||
http.Handle(s.pattern, websocket.Handler(s.onConnection)) |
||||
func (s *ChatServer) Send(payload events.EventPayload, client *ChatClient) { |
||||
data, err := json.Marshal(payload) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return |
||||
} |
||||
|
||||
log.Tracef("Starting the websocket listener on: %s", s.pattern) |
||||
client.send <- data |
||||
} |
||||
|
||||
for { |
||||
select { |
||||
// add new a client
|
||||
case c := <-s.addCh: |
||||
l.Lock() |
||||
s.Clients[c.socketID] = c |
||||
|
||||
if !c.Ignore { |
||||
s.listener.ClientAdded(c.GetViewerClientFromChatClient()) |
||||
s.sendWelcomeMessageToClient(c) |
||||
} |
||||
l.Unlock() |
||||
|
||||
// remove a client
|
||||
case c := <-s.delCh: |
||||
s.removeClient(c) |
||||
case msg := <-s.sendAllCh: |
||||
if data.GetChatDisabled() { |
||||
break |
||||
} |
||||
// DisconnectUser will forcefully disconnect all clients belonging to a user by ID.
|
||||
func (s *ChatServer) DisconnectUser(userID string) { |
||||
s.mu.Lock() |
||||
clients, err := GetClientsForUser(userID) |
||||
s.mu.Unlock() |
||||
|
||||
if err != nil || clients == nil || len(clients) == 0 { |
||||
log.Debugln("Requested to disconnect user", userID, err) |
||||
return |
||||
} |
||||
|
||||
for _, client := range clients { |
||||
log.Traceln("Disconnecting client", client.User.Id, "owned by", client.User.DisplayName) |
||||
|
||||
if !msg.Empty() { |
||||
// set defaults before sending msg to anywhere
|
||||
msg.SetDefaults() |
||||
go func(client *ChatClient) { |
||||
event := events.UserDisabledEvent{} |
||||
event.SetDefaults() |
||||
|
||||
s.listener.MessageSent(msg) |
||||
s.sendAll(msg) |
||||
// Send this disabled event specifically to this single connected client
|
||||
// to let them know they've been banned.
|
||||
_server.Send(event.GetBroadcastPayload(), client) |
||||
|
||||
// Store in the message history
|
||||
if !msg.Ephemeral { |
||||
addMessage(msg) |
||||
} |
||||
// Give the socket time to send out the above message.
|
||||
// Unfortunately I don't know of any way to get a real callback to know when
|
||||
// the message was successfully sent, so give it a couple seconds.
|
||||
time.Sleep(2 * time.Second) |
||||
|
||||
// Send webhooks
|
||||
go webhooks.SendChatEvent(msg) |
||||
// Forcefully disconnect if still valid.
|
||||
if client != nil { |
||||
client.close() |
||||
} |
||||
case ping := <-s.pingCh: |
||||
fmt.Println("PING?", ping) |
||||
}(client) |
||||
} |
||||
} |
||||
|
||||
case err := <-s.errCh: |
||||
log.Trace("Error: ", err.Error()) |
||||
func (s *ChatServer) eventReceived(event chatClientEvent) { |
||||
var typecheck map[string]interface{} |
||||
if err := json.Unmarshal(event.data, &typecheck); err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
|
||||
case <-s.doneCh: |
||||
return |
||||
} |
||||
eventType := typecheck["type"] |
||||
|
||||
switch eventType { |
||||
case events.MessageSent: |
||||
s.userMessageSent(event) |
||||
|
||||
case events.UserNameChanged: |
||||
s.userNameChanged(event) |
||||
|
||||
default: |
||||
log.Debugln(eventType, "event not found:", typecheck) |
||||
} |
||||
} |
||||
|
||||
func (s *server) removeClient(c *Client) { |
||||
l.Lock() |
||||
if _, ok := s.Clients[c.socketID]; ok { |
||||
delete(s.Clients, c.socketID) |
||||
func (s *ChatServer) sendWelcomeMessageToClient(c *ChatClient) { |
||||
// Add an artificial delay so people notice this message come in.
|
||||
time.Sleep(7 * time.Second) |
||||
|
||||
welcomeMessage := utils.RenderSimpleMarkdown(data.GetServerWelcomeMessage()) |
||||
|
||||
s.listener.ClientRemoved(c.socketID) |
||||
log.Tracef("The client was connected for %s and sent %d messages (%s)", time.Since(c.ConnectedAt), c.MessageCount, c.ClientID) |
||||
if welcomeMessage != "" { |
||||
s.sendSystemMessageToClient(c, welcomeMessage) |
||||
} |
||||
l.Unlock() |
||||
} |
||||
|
||||
func (s *server) sendWelcomeMessageToClient(c *Client) { |
||||
go func() { |
||||
// Add an artificial delay so people notice this message come in.
|
||||
time.Sleep(7 * time.Second) |
||||
func (s *ChatServer) sendAllWelcomeMessage() { |
||||
welcomeMessage := utils.RenderSimpleMarkdown(data.GetServerWelcomeMessage()) |
||||
|
||||
welcomeMessage := data.GetServerWelcomeMessage() |
||||
if welcomeMessage != "" { |
||||
initialMessage := models.ChatEvent{ClientID: "owncast-server", Author: data.GetServerName(), Body: welcomeMessage, ID: "initial-message-1", MessageType: "SYSTEM", Visible: true, Timestamp: time.Now()} |
||||
c.write(initialMessage) |
||||
if welcomeMessage != "" { |
||||
clientMessage := events.SystemMessageEvent{ |
||||
Event: events.Event{}, |
||||
MessageEvent: events.MessageEvent{ |
||||
Body: welcomeMessage, |
||||
}, |
||||
} |
||||
}() |
||||
clientMessage.SetDefaults() |
||||
_ = s.Broadcast(clientMessage.GetBroadcastPayload()) |
||||
} |
||||
} |
||||
|
||||
func (s *ChatServer) sendSystemMessageToClient(c *ChatClient, message string) { |
||||
clientMessage := events.SystemMessageEvent{ |
||||
Event: events.Event{}, |
||||
MessageEvent: events.MessageEvent{ |
||||
Body: message, |
||||
}, |
||||
} |
||||
clientMessage.SetDefaults() |
||||
s.Send(clientMessage.GetBroadcastPayload(), c) |
||||
} |
||||
|
||||
func (s *ChatServer) sendActionToClient(c *ChatClient, message string) { |
||||
clientMessage := events.ActionEvent{ |
||||
MessageEvent: events.MessageEvent{ |
||||
Body: message, |
||||
}, |
||||
} |
||||
clientMessage.SetDefaults() |
||||
clientMessage.RenderBody() |
||||
s.Send(clientMessage.GetBroadcastPayload(), c) |
||||
} |
||||
|
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"syscall" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// Set the soft file handler limit as 70% of
|
||||
// the max as the client connection limit.
|
||||
func handleMaxConnectionCount() uint { |
||||
var rLimit syscall.Rlimit |
||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
originalLimit := rLimit.Cur |
||||
// Set the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason.
|
||||
proposedLimit := int(float32(rLimit.Max) * 0.7) |
||||
|
||||
rLimit.Cur = uint64(proposedLimit) |
||||
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
log.Traceln("Max process connection count increased from", originalLimit, "to", proposedLimit) |
||||
|
||||
return uint(float32(rLimit.Cur)) |
||||
} |
@ -1,44 +0,0 @@
@@ -1,44 +0,0 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"github.com/owncast/owncast/core/chat" |
||||
"github.com/owncast/owncast/models" |
||||
) |
||||
|
||||
// ChatListenerImpl the implementation of the chat client.
|
||||
type ChatListenerImpl struct{} |
||||
|
||||
// ClientAdded is for when a client is added the system.
|
||||
func (cl ChatListenerImpl) ClientAdded(client models.Client) { |
||||
SetChatClientActive(client) |
||||
} |
||||
|
||||
// ClientRemoved is for when a client disconnects/is removed.
|
||||
func (cl ChatListenerImpl) ClientRemoved(clientID string) { |
||||
RemoveChatClient(clientID) |
||||
} |
||||
|
||||
// MessageSent is for when a message is sent.
|
||||
func (cl ChatListenerImpl) MessageSent(message models.ChatEvent) { |
||||
} |
||||
|
||||
// IsStreamConnected will return if the stream is connected.
|
||||
func (cl ChatListenerImpl) IsStreamConnected() bool { |
||||
return IsStreamConnected() |
||||
} |
||||
|
||||
// SendMessageToChat sends a message to the chat server.
|
||||
func SendMessageToChat(message models.ChatEvent) error { |
||||
chat.SendMessage(message) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// GetAllChatMessages gets all of the chat messages.
|
||||
func GetAllChatMessages() []models.ChatEvent { |
||||
return chat.GetMessages() |
||||
} |
||||
|
||||
func GetModerationChatMessages() []models.ChatEvent { |
||||
return chat.GetModerationChatMessages() |
||||
} |
@ -1,198 +0,0 @@
@@ -1,198 +0,0 @@
|
||||
package data |
||||
|
||||
import ( |
||||
"errors" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/models" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func createAccessTokensTable() { |
||||
log.Traceln("Creating access_tokens table...") |
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS access_tokens ( |
||||
"token" string NOT NULL PRIMARY KEY, |
||||
"name" string, |
||||
"scopes" TEXT, |
||||
"timestamp" DATETIME DEFAULT CURRENT_TIMESTAMP, |
||||
"last_used" DATETIME |
||||
);` |
||||
|
||||
stmt, err := _db.Prepare(createTableSQL) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer stmt.Close() |
||||
if _, err := stmt.Exec(); err != nil { |
||||
log.Warnln(err) |
||||
} |
||||
} |
||||
|
||||
// InsertToken will add a new token to the database.
|
||||
func InsertToken(token string, name string, scopes []string) error { |
||||
log.Println("Adding new access token:", name) |
||||
|
||||
scopesString := strings.Join(scopes, ",") |
||||
|
||||
tx, err := _db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("INSERT INTO access_tokens(token, name, scopes) values(?, ?, ?)") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(token, name, scopesString); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// DeleteToken will delete a token from the database.
|
||||
func DeleteToken(token string) error { |
||||
log.Println("Deleting access token:", token) |
||||
|
||||
tx, err := _db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("DELETE FROM access_tokens WHERE token = ?") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
result, err := stmt.Exec(token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 { |
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found") |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// DoesTokenSupportScope will determine if a specific token has access to perform a scoped action.
|
||||
func DoesTokenSupportScope(token string, scope string) (bool, error) { |
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
var query = `SELECT count(*) FROM ( |
||||
WITH RECURSIVE split(token, scope, rest) AS ( |
||||
SELECT token, '', scopes || ',' FROM access_tokens |
||||
UNION ALL |
||||
SELECT token, |
||||
substr(rest, 0, instr(rest, ',')), |
||||
substr(rest, instr(rest, ',')+1) |
||||
FROM split |
||||
WHERE rest <> '') |
||||
SELECT token, scope |
||||
FROM split |
||||
WHERE scope <> '' |
||||
ORDER BY token, scope |
||||
) AS token WHERE token.token = ? AND token.scope = ?;` |
||||
|
||||
row := _db.QueryRow(query, token, scope) |
||||
|
||||
var count = 0 |
||||
err := row.Scan(&count) |
||||
|
||||
return count > 0, err |
||||
} |
||||
|
||||
// GetAccessTokens will return all access tokens.
|
||||
func GetAccessTokens() ([]models.AccessToken, error) { //nolint
|
||||
tokens := make([]models.AccessToken, 0) |
||||
|
||||
// Get all messages sent within the past day
|
||||
var query = "SELECT * FROM access_tokens" |
||||
|
||||
rows, err := _db.Query(query) |
||||
if err != nil { |
||||
return tokens, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
for rows.Next() { |
||||
var token string |
||||
var name string |
||||
var scopes string |
||||
var timestampString string |
||||
var lastUsedString *string |
||||
|
||||
if err := rows.Scan(&token, &name, &scopes, ×tampString, &lastUsedString); err != nil { |
||||
log.Error("There is a problem reading the database.", err) |
||||
return tokens, err |
||||
} |
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, timestampString) |
||||
if err != nil { |
||||
return tokens, err |
||||
} |
||||
|
||||
var lastUsed *time.Time = nil |
||||
if lastUsedString != nil { |
||||
lastUsedTime, _ := time.Parse(time.RFC3339, *lastUsedString) |
||||
lastUsed = &lastUsedTime |
||||
} |
||||
|
||||
singleToken := models.AccessToken{ |
||||
Name: name, |
||||
Token: token, |
||||
Scopes: strings.Split(scopes, ","), |
||||
Timestamp: timestamp, |
||||
LastUsed: lastUsed, |
||||
} |
||||
|
||||
tokens = append(tokens, singleToken) |
||||
} |
||||
|
||||
if err := rows.Err(); err != nil { |
||||
return tokens, err |
||||
} |
||||
|
||||
return tokens, nil |
||||
} |
||||
|
||||
// SetAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func SetAccessTokenAsUsed(token string) error { |
||||
tx, err := _db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE access_tokens SET last_used = CURRENT_TIMESTAMP WHERE token = ?") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(token); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
package data |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/teris-io/shortid" |
||||
) |
||||
|
||||
func migrateToSchema1(db *sql.DB) { |
||||
// Since it's just a backlog of chat messages let's wipe the old messages
|
||||
// and recreate the table.
|
||||
|
||||
// Drop the old messages table
|
||||
stmt, err := db.Prepare("DROP TABLE messages") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer stmt.Close() |
||||
_, err = stmt.Exec() |
||||
if err != nil { |
||||
log.Warnln(err) |
||||
} |
||||
|
||||
// Recreate it
|
||||
createUsersTable(db) |
||||
|
||||
// Migrate access tokens to become chat users
|
||||
type oldAccessToken struct { |
||||
accessToken string |
||||
displayName string |
||||
scopes string |
||||
createdAt time.Time |
||||
lastUsedAt *time.Time |
||||
} |
||||
|
||||
oldAccessTokens := make([]oldAccessToken, 0) |
||||
|
||||
query := `SELECT * FROM access_tokens` |
||||
|
||||
rows, err := db.Query(query) |
||||
if err != nil || rows.Err() != nil { |
||||
log.Errorln("error migrating access tokens to schema v1", err, rows.Err()) |
||||
return |
||||
} |
||||
defer rows.Close() |
||||
|
||||
for rows.Next() { |
||||
var token string |
||||
var name string |
||||
var scopes string |
||||
var timestampString string |
||||
var lastUsedString *string |
||||
|
||||
if err := rows.Scan(&token, &name, &scopes, ×tampString, &lastUsedString); err != nil { |
||||
log.Error("There is a problem reading the database.", err) |
||||
return |
||||
} |
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, timestampString) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
var lastUsed *time.Time = nil |
||||
if lastUsedString != nil { |
||||
lastUsedTime, _ := time.Parse(time.RFC3339, *lastUsedString) |
||||
lastUsed = &lastUsedTime |
||||
} |
||||
|
||||
oldToken := oldAccessToken{ |
||||
accessToken: token, |
||||
displayName: name, |
||||
scopes: scopes, |
||||
createdAt: timestamp, |
||||
lastUsedAt: lastUsed, |
||||
} |
||||
|
||||
oldAccessTokens = append(oldAccessTokens, oldToken) |
||||
} |
||||
|
||||
// Recreate them as users
|
||||
for _, token := range oldAccessTokens { |
||||
color := utils.GenerateRandomDisplayColor() |
||||
if err := insertAPIToken(db, token.accessToken, token.displayName, color, token.scopes); err != nil { |
||||
log.Errorln("Error migrating access token", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func insertAPIToken(db *sql.DB, token string, name string, color int, scopes string) error { |
||||
log.Debugln("Adding new access token:", name) |
||||
|
||||
id := shortid.MustGenerate() |
||||
|
||||
tx, err := db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("INSERT INTO users(id, access_token, display_name, display_color, scopes, type) values(?, ?, ?, ?, ?, ?)") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err = stmt.Exec(id, token, name, color, scopes, "API"); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
package data |
||||
|
||||
import ( |
||||
"database/sql" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func createUsersTable(db *sql.DB) { |
||||
log.Traceln("Creating users table...") |
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS users ( |
||||
"id" TEXT, |
||||
"access_token" string NOT NULL, |
||||
"display_name" TEXT NOT NULL, |
||||
"display_color" NUMBER NOT NULL, |
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
"disabled_at" TIMESTAMP, |
||||
"previous_names" TEXT DEFAULT '', |
||||
"namechanged_at" TIMESTAMP, |
||||
"scopes" TEXT, |
||||
"type" TEXT DEFAULT 'STANDARD', |
||||
"last_used" DATETIME DEFAULT CURRENT_TIMESTAMP, |
||||
PRIMARY KEY (id, access_token), |
||||
UNIQUE(id, access_token) |
||||
);CREATE INDEX index ON users (id, access_token)` |
||||
|
||||
stmt, err := db.Prepare(createTableSQL) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer stmt.Close() |
||||
_, err = stmt.Exec() |
||||
if err != nil { |
||||
log.Warnln(err) |
||||
} |
||||
} |
@ -0,0 +1,264 @@
@@ -0,0 +1,264 @@
|
||||
package user |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"errors" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/utils" |
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/teris-io/shortid" |
||||
) |
||||
|
||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||
type ExternalAPIUser struct { |
||||
Id string `json:"id"` |
||||
AccessToken string `json:"accessToken"` |
||||
DisplayName string `json:"displayName"` |
||||
DisplayColor int `json:"displayColor"` |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
Scopes []string `json:"scopes"` |
||||
Type string `json:"type,omitempty"` // Should be API
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` |
||||
} |
||||
|
||||
const ( |
||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES" |
||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES" |
||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS" |
||||
) |
||||
|
||||
// For a scope to be seen as "valid" it must live in this slice.
|
||||
var validAccessTokenScopes = []string{ |
||||
ScopeCanSendChatMessages, |
||||
ScopeCanSendSystemMessages, |
||||
ScopeHasAdminAccess, |
||||
} |
||||
|
||||
// InsertToken will add a new token to the database.
|
||||
func InsertExternalAPIUser(token string, name string, color int, scopes []string) error { |
||||
log.Traceln("Adding new API user:", name) |
||||
|
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
scopesString := strings.Join(scopes, ",") |
||||
id := shortid.MustGenerate() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("INSERT INTO users(id, access_token, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?, ?)") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err = stmt.Exec(id, token, name, color, scopesString, "API", name); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// DeleteExternalAPIUser will delete a token from the database.
|
||||
func DeleteExternalAPIUser(token string) error { |
||||
log.Traceln("Deleting access token:", token) |
||||
|
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = ? WHERE access_token = ?") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
result, err := stmt.Exec(time.Now(), token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 { |
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found") |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
||||
func GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*ExternalAPIUser, error) { |
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
var query = `SELECT id, access_token, scopes, display_name, display_color, created_at, last_used FROM ( |
||||
WITH RECURSIVE split(id, access_token, scopes, display_name, display_color, created_at, last_used, disabled_at, scope, rest) AS ( |
||||
SELECT id, access_token, scopes, display_name, display_color, created_at, last_used, disabled_at, '', scopes || ',' FROM users |
||||
UNION ALL |
||||
SELECT id, access_token, scopes, display_name, display_color, created_at, last_used, disabled_at, |
||||
substr(rest, 0, instr(rest, ',')), |
||||
substr(rest, instr(rest, ',')+1) |
||||
FROM split |
||||
WHERE rest <> '') |
||||
SELECT id, access_token, scopes, display_name, display_color, created_at, last_used, disabled_at, scope |
||||
FROM split |
||||
WHERE scope <> '' |
||||
ORDER BY access_token, scope |
||||
) AS token WHERE token.access_token = ? AND token.scope = ?` |
||||
|
||||
row := _datastore.DB.QueryRow(query, token, scope) |
||||
integration, err := makeExternalAPIUserFromRow(row) |
||||
|
||||
return integration, err |
||||
} |
||||
|
||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
||||
func GetIntegrationNameForAccessToken(token string) *string { |
||||
query := "SELECT display_name FROM users WHERE access_token IS ? AND disabled_at IS NULL" |
||||
row := _datastore.DB.QueryRow(query, token) |
||||
|
||||
var name string |
||||
err := row.Scan(&name) |
||||
|
||||
if err != nil { |
||||
log.Warnln(err) |
||||
return nil |
||||
} |
||||
|
||||
return &name |
||||
} |
||||
|
||||
// GetExternalAPIUser will return all access tokens.
|
||||
func GetExternalAPIUser() ([]ExternalAPIUser, error) { //nolint
|
||||
// Get all messages sent within the past day
|
||||
var query = "SELECT id, access_token, display_name, display_color, scopes, created_at, last_used FROM users WHERE type IS 'API' AND disabled_at IS NULL" |
||||
|
||||
rows, err := _datastore.DB.Query(query) |
||||
if err != nil { |
||||
return []ExternalAPIUser{}, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
integrations, err := makeExternalAPIUsersFromRows(rows) |
||||
|
||||
return integrations, err |
||||
} |
||||
|
||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func SetExternalAPIUserAccessTokenAsUsed(token string) error { |
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE access_token = ?") |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(token); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = tx.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func makeExternalAPIUserFromRow(row *sql.Row) (*ExternalAPIUser, error) { |
||||
var id string |
||||
var accessToken string |
||||
var displayName string |
||||
var displayColor int |
||||
var scopes string |
||||
var createdAt time.Time |
||||
var lastUsedAt *time.Time |
||||
|
||||
err := row.Scan(&id, &accessToken, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil, err |
||||
} |
||||
|
||||
integration := ExternalAPIUser{ |
||||
Id: id, |
||||
AccessToken: accessToken, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
Scopes: strings.Split(scopes, ","), |
||||
LastUsedAt: lastUsedAt, |
||||
} |
||||
|
||||
return &integration, nil |
||||
} |
||||
|
||||
func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) { |
||||
integrations := make([]ExternalAPIUser, 0) |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var accessToken string |
||||
var displayName string |
||||
var displayColor int |
||||
var scopes string |
||||
var createdAt time.Time |
||||
var lastUsedAt *time.Time |
||||
|
||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil, err |
||||
} |
||||
|
||||
integration := ExternalAPIUser{ |
||||
Id: id, |
||||
AccessToken: accessToken, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
Scopes: strings.Split(scopes, ","), |
||||
LastUsedAt: lastUsedAt, |
||||
} |
||||
integrations = append(integrations, integration) |
||||
} |
||||
|
||||
return integrations, nil |
||||
} |
||||
|
||||
// HasValidScopes will verify that all the scopes provided are valid.
|
||||
func HasValidScopes(scopes []string) bool { |
||||
for _, scope := range scopes { |
||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope) |
||||
if !foundInSlice { |
||||
log.Errorln("Invalid scope", scope) |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
@ -0,0 +1,261 @@
@@ -0,0 +1,261 @@
|
||||
package user |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/utils" |
||||
"github.com/teris-io/shortid" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
var _datastore *data.Datastore |
||||
|
||||
type User struct { |
||||
Id string `json:"id"` |
||||
AccessToken string `json:"-"` |
||||
DisplayName string `json:"displayName"` |
||||
DisplayColor int `json:"displayColor"` |
||||
CreatedAt time.Time `json:"createdAt"` |
||||
DisabledAt *time.Time `json:"disabledAt,omitempty"` |
||||
PreviousNames []string `json:"previousNames"` |
||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"` |
||||
} |
||||
|
||||
func (u *User) IsEnabled() bool { |
||||
return u.DisabledAt == nil |
||||
} |
||||
|
||||
func SetupUsers() { |
||||
_datastore = data.GetDatastore() |
||||
} |
||||
|
||||
func CreateAnonymousUser(username string) (*User, error) { |
||||
id := shortid.MustGenerate() |
||||
accessToken, err := utils.GenerateAccessToken() |
||||
if err != nil { |
||||
log.Errorln("Unable to create access token for new user") |
||||
return nil, err |
||||
} |
||||
|
||||
var displayName = username |
||||
if displayName == "" { |
||||
displayName = utils.GeneratePhrase() |
||||
} |
||||
|
||||
displayColor := utils.GenerateRandomDisplayColor() |
||||
|
||||
user := &User{ |
||||
Id: id, |
||||
AccessToken: accessToken, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: time.Now(), |
||||
} |
||||
|
||||
if err := create(user); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return user, nil |
||||
} |
||||
|
||||
func ChangeUsername(userId string, username string) { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
|
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
if err := tx.Rollback(); err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
}() |
||||
|
||||
stmt, err := tx.Prepare("UPDATE users SET display_name = ?, previous_names = previous_names || ?, namechanged_at = ? WHERE id = ?") |
||||
|
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
_, err = stmt.Exec(username, fmt.Sprintf(",%s", username), time.Now(), userId) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
|
||||
if err := tx.Commit(); err != nil { |
||||
log.Errorln("error changing display name of user", userId, err) |
||||
} |
||||
} |
||||
|
||||
func create(user *User) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, access_token, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?, ?)") |
||||
|
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
_, err = stmt.Exec(user.Id, user.AccessToken, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt) |
||||
if err != nil { |
||||
log.Errorln("error creating new user", err) |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
func SetEnabled(userID string, enabled bool) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
var stmt *sql.Stmt |
||||
if !enabled { |
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?") |
||||
} else { |
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?") |
||||
} |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer stmt.Close() |
||||
|
||||
if _, err := stmt.Exec(userID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// GetUserByToken will return a user by an access token.
|
||||
func GetUserByToken(token string) *User { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE access_token = ?" |
||||
row := _datastore.DB.QueryRow(query, token) |
||||
|
||||
return getUserFromRow(row) |
||||
} |
||||
|
||||
// GetUserById will return a user by a user ID.
|
||||
func GetUserById(id string) *User { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE id = ?" |
||||
row := _datastore.DB.QueryRow(query, id) |
||||
if row == nil { |
||||
log.Errorln(row) |
||||
return nil |
||||
} |
||||
return getUserFromRow(row) |
||||
} |
||||
|
||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
||||
func GetDisabledUsers() []*User { |
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'" |
||||
|
||||
rows, err := _datastore.DB.Query(query) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return nil |
||||
} |
||||
defer rows.Close() |
||||
|
||||
users := getUsersFromRows(rows) |
||||
|
||||
sort.Slice(users, func(i, j int) bool { |
||||
return users[i].DisabledAt.Before(*users[j].DisabledAt) |
||||
}) |
||||
|
||||
return users |
||||
} |
||||
|
||||
func getUsersFromRows(rows *sql.Rows) []*User { |
||||
users := make([]*User, 0) |
||||
|
||||
for rows.Next() { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var createdAt time.Time |
||||
var disabledAt *time.Time |
||||
var previousUsernames string |
||||
var userNameChangedAt *time.Time |
||||
|
||||
if err := rows.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil { |
||||
log.Errorln("error creating collection of users from results", err) |
||||
return nil |
||||
} |
||||
|
||||
user := &User{ |
||||
Id: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(previousUsernames, ","), |
||||
NameChangedAt: userNameChangedAt, |
||||
} |
||||
users = append(users, user) |
||||
} |
||||
|
||||
sort.Slice(users, func(i, j int) bool { |
||||
return users[i].CreatedAt.Before(users[j].CreatedAt) |
||||
}) |
||||
|
||||
return users |
||||
} |
||||
|
||||
func getUserFromRow(row *sql.Row) *User { |
||||
var id string |
||||
var displayName string |
||||
var displayColor int |
||||
var createdAt time.Time |
||||
var disabledAt *time.Time |
||||
var previousUsernames string |
||||
var userNameChangedAt *time.Time |
||||
|
||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil { |
||||
return nil |
||||
} |
||||
|
||||
return &User{ |
||||
Id: id, |
||||
DisplayName: displayName, |
||||
DisplayColor: displayColor, |
||||
CreatedAt: createdAt, |
||||
DisabledAt: disabledAt, |
||||
PreviousNames: strings.Split(previousUsernames, ","), |
||||
NameChangedAt: userNameChangedAt, |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
@ -1,53 +0,0 @@
@@ -1,53 +0,0 @@
|
||||
package models |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
const ( |
||||
// ScopeCanSendUserMessages will allow sending chat messages as users.
|
||||
ScopeCanSendUserMessages = "CAN_SEND_MESSAGES" |
||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES" |
||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS" |
||||
) |
||||
|
||||
// For a scope to be seen as "valid" it must live in this slice.
|
||||
var validAccessTokenScopes = []string{ |
||||
ScopeCanSendUserMessages, |
||||
ScopeCanSendSystemMessages, |
||||
ScopeHasAdminAccess, |
||||
} |
||||
|
||||
// AccessToken gives access to 3rd party code to access specific Owncast APIs.
|
||||
type AccessToken struct { |
||||
Token string `json:"token"` |
||||
Name string `json:"name"` |
||||
Scopes []string `json:"scopes"` |
||||
Timestamp time.Time `json:"timestamp"` |
||||
LastUsed *time.Time `json:"lastUsed"` |
||||
} |
||||
|
||||
// HasValidScopes will verify that all the scopes provided are valid.
|
||||
// This is not a efficient method.
|
||||
func HasValidScopes(scopes []string) bool { |
||||
for _, scope := range scopes { |
||||
if !findItemInSlice(validAccessTokenScopes, scope) { |
||||
log.Errorln("Invalid scope", scope) |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func findItemInSlice(slice []string, value string) bool { |
||||
for _, item := range slice { |
||||
if item == value { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
package models |
||||
|
||||
// ChatListener represents the listener for the chat server.
|
||||
type ChatListener interface { |
||||
ClientAdded(client Client) |
||||
ClientRemoved(clientID string) |
||||
MessageSent(message ChatEvent) |
||||
IsStreamConnected() bool |
||||
} |
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
package models |
||||
|
||||
// NameChangeEvent represents a user changing their name in chat.
|
||||
type NameChangeEvent struct { |
||||
OldName string `json:"oldName"` |
||||
NewName string `json:"newName"` |
||||
Image string `json:"image"` |
||||
Type EventType `json:"type"` |
||||
ID string `json:"id"` |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
const { test } = require('@jest/globals'); |
||||
var request = require('supertest'); |
||||
request = request('http://127.0.0.1:8080'); |
||||
|
||||
const registerChat = require('./lib/chat').registerChat; |
||||
const sendChatMessage = require('./lib/chat').sendChatMessage; |
||||
|
||||
const testVisibilityMessage = { |
||||
body: "message " + Math.floor(Math.random() * 100), |
||||
type: 'CHAT', |
||||
}; |
||||
|
||||
var userId |
||||
var accessToken |
||||
test('can register a user', async (done) => { |
||||
const registration = await registerChat(); |
||||
userId = registration.id; |
||||
accessToken = registration.accessToken; |
||||
done(); |
||||
}); |
||||
|
||||
test('can send a chat message', async (done) => { |
||||
sendChatMessage(testVisibilityMessage, accessToken, done); |
||||
}); |
||||
|
||||
test('can disable a user', async (done) => { |
||||
// To allow for visually being able to see the test hiding the
|
||||
// message add a short delay.
|
||||
await new Promise((r) => setTimeout(r, 1500)); |
||||
|
||||
await request.post('/api/admin/chat/users/setenabled').send({ "userId": userId, "enabled": false }) |
||||
.auth('admin', 'abc123').expect(200); |
||||
done(); |
||||
}); |
||||
|
||||
test('verify user is disabled', async (done) => { |
||||
const response = await request.get('/api/admin/chat/users/disabled').auth('admin', 'abc123').expect(200); |
||||
const tokenCheck = response.body.filter((user) => user.id === userId) |
||||
expect(tokenCheck).toHaveLength(1); |
||||
done(); |
||||
}); |
||||
|
||||
test('verify messages from user are hidden', async (done) => { |
||||
const response = await request.get('/api/admin/chat/messages') |
||||
.auth('admin', 'abc123') |
||||
.expect(200); |
||||
const message = response.body.filter(obj => { |
||||
return obj.user.id === userId; |
||||
}); |
||||
expect(message[0].hiddenAt).toBeTruthy(); |
||||
done(); |
||||
}); |
||||
|
||||
test('can re-enable a user', async (done) => { |
||||
await request.post('/api/admin/chat/users/setenabled').send({ "userId": userId, "enabled": true }) |
||||
.auth('admin', 'abc123').expect(200); |
||||
done(); |
||||
}); |
||||
|
||||
test('verify user is enabled', async (done) => { |
||||
const response = await request.get('/api/admin/chat/users/disabled').auth('admin', 'abc123').expect(200); |
||||
const tokenCheck = response.body.filter((user) => user.id === userId) |
||||
expect(tokenCheck).toHaveLength(0); |
||||
|
||||
done(); |
||||
}); |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
var request = require('supertest'); |
||||
request = request('http://127.0.0.1:8080'); |
||||
const WebSocket = require('ws'); |
||||
|
||||
async function registerChat() { |
||||
try { |
||||
const response = await request.post('/api/chat/register'); |
||||
return response.body; |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
function sendChatMessage(message, accessToken, done) { |
||||
const ws = new WebSocket( |
||||
`ws://localhost:8080/ws?accessToken=${accessToken}`, |
||||
{ |
||||
origin: 'http://localhost:8080', |
||||
} |
||||
); |
||||
|
||||
function onOpen() { |
||||
ws.send(JSON.stringify(message), function () { |
||||
ws.close(); |
||||
done(); |
||||
}); |
||||
} |
||||
|
||||
ws.on('open', onOpen); |
||||
} |
||||
|
||||
module.exports.sendChatMessage = sendChatMessage; |
||||
module.exports.registerChat = registerChat; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
const WebSocket = require('ws'); |
||||
const fetch = require('node-fetch'); |
||||
|
||||
var connectionCount = 0; |
||||
const targetConnectionCount = 5000; |
||||
|
||||
const messages = [ |
||||
'I am a test message', |
||||
'this is fake', |
||||
'i write emoji 😀', |
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', |
||||
'Sed pulvinar proin gravida hendrerit. Mauris in aliquam sem fringilla ut morbi tincidunt augue. In cursus turpis massa tincidunt dui.', |
||||
'Feugiat in ante metus dictum at tempor commodo ullamcorper. Nunc aliquet bibendum enim facilisis gravida neque convallis a. Vitae tortor condimentum lacinia quis vel eros donec ac odio.', |
||||
'Here is _some_ **markdown**!', |
||||
]; |
||||
|
||||
var availableMessages = messages.slice(); |
||||
|
||||
|
||||
async function registerChat() { |
||||
const options = { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json' |
||||
} |
||||
} |
||||
|
||||
try { |
||||
const response = await fetch('http://localhost:8080/api/chat/register', options); |
||||
const result = await response.json(); |
||||
return result; |
||||
} catch(e) { |
||||
console.error(e); |
||||
} |
||||
} |
||||
|
||||
async function runSingleUserIteration() { |
||||
const registration = await registerChat(); |
||||
const accessToken = registration.accessToken; |
||||
|
||||
function sendTestMessage() { |
||||
if (availableMessages.length == 0) { |
||||
availableMessages = messages.slice(); |
||||
} |
||||
|
||||
const messageIndex = Math.floor(Math.random() * availableMessages.length); |
||||
const message = availableMessages[messageIndex]; |
||||
availableMessages.splice(messageIndex, 1); |
||||
|
||||
const testMessage = { |
||||
body: message, |
||||
type: 'CHAT', |
||||
}; |
||||
|
||||
ws.send(JSON.stringify(testMessage)); |
||||
|
||||
// After this message is sent then run it again.
|
||||
setTimeout(runSingleUserIteration, 20); |
||||
} |
||||
|
||||
const ws = new WebSocket(`ws://localhost:8080/ws?accessToken=${accessToken}`, { |
||||
origin: 'http://localhost:8080', |
||||
}); |
||||
|
||||
// When the websocket connects then send a chat message.
|
||||
ws.on('open', function open() { |
||||
connectionCount++; |
||||
console.log(connectionCount + '/' + targetConnectionCount, " chat clients.") |
||||
if (connectionCount === targetConnectionCount) { |
||||
process.exit(); |
||||
} |
||||
setTimeout(sendTestMessage, 5); |
||||
}); |
||||
|
||||
ws.on('error', function incoming(data) { |
||||
console.error(data); |
||||
}); |
||||
} |
||||
|
||||
runSingleUserIteration(); |
@ -1,14 +0,0 @@
@@ -1,14 +0,0 @@
|
||||
module.exports = { createTestMessageObject }; |
||||
|
||||
function createTestMessageObject(userContext, events, done) { |
||||
const randomNumber = Math.floor((Math.random() * 10) + 1); |
||||
const author = "load-test-user-" + randomNumber |
||||
const data = { |
||||
author: author, |
||||
body: "Test 12345. " + randomNumber, |
||||
type: "CHAT" |
||||
}; |
||||
// set the "data" variable for the virtual user to use in the subsequent action
|
||||
userContext.vars.data = data; |
||||
return done(); |
||||
} |
@ -1,32 +0,0 @@
@@ -1,32 +0,0 @@
|
||||
config: |
||||
target: "ws://localhost:8080/entry" |
||||
processor: "./websocketTest.js" |
||||
|
||||
ensure: |
||||
p95: 200 |
||||
maxErrorRate: 1 |
||||
|
||||
phases: |
||||
- duration: 30 |
||||
arrivalRate: 5 |
||||
rampTo: 5 |
||||
name: "Warming up" |
||||
- duration: 240 |
||||
arrivalRate: 5 |
||||
rampTo: 40 |
||||
name: "Max load" |
||||
|
||||
ws: |
||||
subprotocols: |
||||
- json |
||||
headers: |
||||
Connection: Upgrade |
||||
Origin: http://localhost:8080 |
||||
Sec-WebSocket-Version: 13 |
||||
|
||||
scenarios: |
||||
- engine: "ws" |
||||
flow: |
||||
- function: "createTestMessageObject" |
||||
- send: "{{ data }}" |
||||
- think: 30 # Each client should stay connected for 30 seconds |
@ -0,0 +1,636 @@
@@ -0,0 +1,636 @@
|
||||
package utils |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math/rand" |
||||
"time" |
||||
) |
||||
|
||||
// Name generator values from https://raw.githubusercontent.com/railroadmanuk/random_names/master/random_names.go
|
||||
|
||||
var ( |
||||
// taken from https://github.com/docker/docker/blob/master/pkg/namesgenerator/names-generator.go
|
||||
left = [...]string{ |
||||
"admiring", |
||||
"adoring", |
||||
"affectionate", |
||||
"agitated", |
||||
"amazing", |
||||
"angry", |
||||
"awesome", |
||||
"blissful", |
||||
"boring", |
||||
"brave", |
||||
"clever", |
||||
"cocky", |
||||
"compassionate", |
||||
"competent", |
||||
"condescending", |
||||
"confident", |
||||
"cranky", |
||||
"dark", |
||||
"dazzling", |
||||
"determined", |
||||
"distracted", |
||||
"dope", |
||||
"dreamy", |
||||
"eager", |
||||
"ecstatic", |
||||
"elastic", |
||||
"elated", |
||||
"elegant", |
||||
"eloquent", |
||||
"epic", |
||||
"fervent", |
||||
"festive", |
||||
"flamboyant", |
||||
"fly", |
||||
"focused", |
||||
"friendly", |
||||
"frosty", |
||||
"gallant", |
||||
"gifted", |
||||
"goofy", |
||||
"goth", |
||||
"gracious", |
||||
"happy", |
||||
"hardcore", |
||||
"heuristic", |
||||
"hopeful", |
||||
"hungry", |
||||
"industrial", |
||||
"infallible", |
||||
"inspiring", |
||||
"jolly", |
||||
"jovial", |
||||
"keen", |
||||
"kind", |
||||
"laughing", |
||||
"loving", |
||||
"lucid", |
||||
"mystifying", |
||||
"modest", |
||||
"musing", |
||||
"naughty", |
||||
"nervous", |
||||
"nifty", |
||||
"nostalgic", |
||||
"objective", |
||||
"optimistic", |
||||
"peaceful", |
||||
"pedantic", |
||||
"pensive", |
||||
"practical", |
||||
"priceless", |
||||
"quirky", |
||||
"quizzical", |
||||
"radical", |
||||
"relaxed", |
||||
"reverent", |
||||
"romantic", |
||||
"sad", |
||||
"serene", |
||||
"sharp", |
||||
"silly", |
||||
"sleepy", |
||||
"stoic", |
||||
"stupefied", |
||||
"suspicious", |
||||
"tender", |
||||
"thirsty", |
||||
"trusting", |
||||
"ultimate", |
||||
"unruffled", |
||||
"upbeat", |
||||
"vibrant", |
||||
"vigilant", |
||||
"vigorous", |
||||
"wizardly", |
||||
"wonderful", |
||||
"xenodochial", |
||||
"youthful", |
||||
"zealous", |
||||
"zen", |
||||
} |
||||
|
||||
// Docker, starting from 0.7.x, generates names from notable scientists and hackers.
|
||||
// Please, for any amazing man that you add to the list, consider adding an equally amazing woman to it, and vice versa.
|
||||
right = [...]string{ |
||||
// Muhammad ibn Jābir al-Ḥarrānī al-Battānī was a founding father of astronomy. https://en.wikipedia.org/wiki/Mu%E1%B8%A5ammad_ibn_J%C4%81bir_al-%E1%B8%A4arr%C4%81n%C4%AB_al-Batt%C4%81n%C4%AB
|
||||
"albattani", |
||||
|
||||
// Frances E. Allen, became the first female IBM Fellow in 1989. In 2006, she became the first female recipient of the ACM's Turing Award. https://en.wikipedia.org/wiki/Frances_E._Allen
|
||||
"allen", |
||||
|
||||
// June Almeida - Scottish virologist who took the first pictures of the rubella virus - https://en.wikipedia.org/wiki/June_Almeida
|
||||
"almeida", |
||||
|
||||
// Maria Gaetana Agnesi - Italian mathematician, philosopher, theologian and humanitarian. She was the first woman to write a mathematics handbook and the first woman appointed as a Mathematics Professor at a University. https://en.wikipedia.org/wiki/Maria_Gaetana_Agnesi
|
||||
"agnesi", |
||||
|
||||
// Archimedes was a physicist, engineer and mathematician who invented too many things to list them here. https://en.wikipedia.org/wiki/Archimedes
|
||||
"archimedes", |
||||
|
||||
// Maria Ardinghelli - Italian translator, mathematician and physicist - https://en.wikipedia.org/wiki/Maria_Ardinghelli
|
||||
"ardinghelli", |
||||
|
||||
// Aryabhata - Ancient Indian mathematician-astronomer during 476-550 CE https://en.wikipedia.org/wiki/Aryabhata
|
||||
"aryabhata", |
||||
|
||||
// Wanda Austin - Wanda Austin is the President and CEO of The Aerospace Corporation, a leading architect for the US security space programs. https://en.wikipedia.org/wiki/Wanda_Austin
|
||||
"austin", |
||||
|
||||
// Charles Babbage invented the concept of a programmable computer. https://en.wikipedia.org/wiki/Charles_Babbage.
|
||||
"babbage", |
||||
|
||||
// Stefan Banach - Polish mathematician, was one of the founders of modern functional analysis. https://en.wikipedia.org/wiki/Stefan_Banach
|
||||
"banach", |
||||
|
||||
// John Bardeen co-invented the transistor - https://en.wikipedia.org/wiki/John_Bardeen
|
||||
"bardeen", |
||||
|
||||
// Jean Bartik, born Betty Jean Jennings, was one of the original programmers for the ENIAC computer. https://en.wikipedia.org/wiki/Jean_Bartik
|
||||
"bartik", |
||||
|
||||
// Laura Bassi, the world's first female professor https://en.wikipedia.org/wiki/Laura_Bassi
|
||||
"bassi", |
||||
|
||||
// Hugh Beaver, British engineer, founder of the Guinness Book of World Records https://en.wikipedia.org/wiki/Hugh_Beaver
|
||||
"beaver", |
||||
|
||||
// Alexander Graham Bell - an eminent Scottish-born scientist, inventor, engineer and innovator who is credited with inventing the first practical telephone - https://en.wikipedia.org/wiki/Alexander_Graham_Bell
|
||||
"bell", |
||||
|
||||
// Karl Friedrich Benz - a German automobile engineer. Inventor of the first practical motorcar. https://en.wikipedia.org/wiki/Karl_Benz
|
||||
"benz", |
||||
|
||||
// Homi J Bhabha - was an Indian nuclear physicist, founding director, and professor of physics at the Tata Institute of Fundamental Research. Colloquially known as "father of Indian nuclear programme"- https://en.wikipedia.org/wiki/Homi_J._Bhabha
|
||||
"bhabha", |
||||
|
||||
// Bhaskara II - Ancient Indian mathematician-astronomer whose work on calculus predates Newton and Leibniz by over half a millennium - https://en.wikipedia.org/wiki/Bh%C4%81skara_II#Calculus
|
||||
"bhaskara", |
||||
|
||||
// Elizabeth Blackwell - American doctor and first American woman to receive a medical degree - https://en.wikipedia.org/wiki/Elizabeth_Blackwell
|
||||
"blackwell", |
||||
|
||||
// Niels Bohr is the father of quantum theory. https://en.wikipedia.org/wiki/Niels_Bohr.
|
||||
"bohr", |
||||
|
||||
// Kathleen Booth, she's credited with writing the first assembly language. https://en.wikipedia.org/wiki/Kathleen_Booth
|
||||
"booth", |
||||
|
||||
// Anita Borg - Anita Borg was the founding director of the Institute for Women and Technology (IWT). https://en.wikipedia.org/wiki/Anita_Borg
|
||||
"borg", |
||||
|
||||
// Satyendra Nath Bose - He provided the foundation for Bose–Einstein statistics and the theory of the Bose–Einstein condensate. - https://en.wikipedia.org/wiki/Satyendra_Nath_Bose
|
||||
"bose", |
||||
|
||||
// Evelyn Boyd Granville - She was one of the first African-American woman to receive a Ph.D. in mathematics; she earned it in 1949 from Yale University. https://en.wikipedia.org/wiki/Evelyn_Boyd_Granville
|
||||
"boyd", |
||||
|
||||
// Brahmagupta - Ancient Indian mathematician during 598-670 CE who gave rules to compute with zero - https://en.wikipedia.org/wiki/Brahmagupta#Zero
|
||||
"brahmagupta", |
||||
|
||||
// Walter Houser Brattain co-invented the transistor - https://en.wikipedia.org/wiki/Walter_Houser_Brattain
|
||||
"brattain", |
||||
|
||||
// Emmett Brown invented time travel. https://en.wikipedia.org/wiki/Emmett_Brown (thanks Brian Goff)
|
||||
"brown", |
||||
|
||||
// Rachel Carson - American marine biologist and conservationist, her book Silent Spring and other writings are credited with advancing the global environmental movement. https://en.wikipedia.org/wiki/Rachel_Carson
|
||||
"carson", |
||||
|
||||
// Subrahmanyan Chandrasekhar - Astrophysicist known for his mathematical theory on different stages and evolution in structures of the stars. He has won nobel prize for physics - https://en.wikipedia.org/wiki/Subrahmanyan_Chandrasekhar
|
||||
"chandrasekhar", |
||||
|
||||
//Claude Shannon - The father of information theory and founder of digital circuit design theory. (https://en.wikipedia.org/wiki/Claude_Shannon)
|
||||
"shannon", |
||||
|
||||
// Joan Clarke - Bletchley Park code breaker during the Second World War who pioneered techniques that remained top secret for decades. Also an accomplished numismatist https://en.wikipedia.org/wiki/Joan_Clarke
|
||||
"clarke", |
||||
|
||||
// Jane Colden - American botanist widely considered the first female American botanist - https://en.wikipedia.org/wiki/Jane_Colden
|
||||
"colden", |
||||
|
||||
// Gerty Theresa Cori - American biochemist who became the third woman—and first American woman—to win a Nobel Prize in science, and the first woman to be awarded the Nobel Prize in Physiology or Medicine. Cori was born in Prague. https://en.wikipedia.org/wiki/Gerty_Cori
|
||||
"cori", |
||||
|
||||
// Seymour Roger Cray was an American electrical engineer and supercomputer architect who designed a series of computers that were the fastest in the world for decades. https://en.wikipedia.org/wiki/Seymour_Cray
|
||||
"cray", |
||||
|
||||
// This entry reflects a husband and wife team who worked together:
|
||||
// Joan Curran was a Welsh scientist who developed radar and invented chaff, a radar countermeasure. https://en.wikipedia.org/wiki/Joan_Curran
|
||||
// Samuel Curran was an Irish physicist who worked alongside his wife during WWII and invented the proximity fuse. https://en.wikipedia.org/wiki/Samuel_Curran
|
||||
"curran", |
||||
|
||||
// Marie Curie discovered radioactivity. https://en.wikipedia.org/wiki/Marie_Curie.
|
||||
"curie", |
||||
|
||||
// Charles Darwin established the principles of natural evolution. https://en.wikipedia.org/wiki/Charles_Darwin.
|
||||
"darwin", |
||||
|
||||
// Leonardo Da Vinci invented too many things to list here. https://en.wikipedia.org/wiki/Leonardo_da_Vinci.
|
||||
"davinci", |
||||
|
||||
// Edsger Wybe Dijkstra was a Dutch computer scientist and mathematical scientist. https://en.wikipedia.org/wiki/Edsger_W._Dijkstra.
|
||||
"dijkstra", |
||||
|
||||
// Donna Dubinsky - played an integral role in the development of personal digital assistants (PDAs) serving as CEO of Palm, Inc. and co-founding Handspring. https://en.wikipedia.org/wiki/Donna_Dubinsky
|
||||
"dubinsky", |
||||
|
||||
// Annie Easley - She was a leading member of the team which developed software for the Centaur rocket stage and one of the first African-Americans in her field. https://en.wikipedia.org/wiki/Annie_Easley
|
||||
"easley", |
||||
|
||||
// Thomas Alva Edison, prolific inventor https://en.wikipedia.org/wiki/Thomas_Edison
|
||||
"edison", |
||||
|
||||
// Albert Einstein invented the general theory of relativity. https://en.wikipedia.org/wiki/Albert_Einstein
|
||||
"einstein", |
||||
|
||||
// Gertrude Elion - American biochemist, pharmacologist and the 1988 recipient of the Nobel Prize in Medicine - https://en.wikipedia.org/wiki/Gertrude_Elion
|
||||
"elion", |
||||
|
||||
// Douglas Engelbart gave the mother of all demos: https://en.wikipedia.org/wiki/Douglas_Engelbart
|
||||
"engelbart", |
||||
|
||||
// Euclid invented geometry. https://en.wikipedia.org/wiki/Euclid
|
||||
"euclid", |
||||
|
||||
// Leonhard Euler invented large parts of modern mathematics. https://de.wikipedia.org/wiki/Leonhard_Euler
|
||||
"euler", |
||||
|
||||
// Pierre de Fermat pioneered several aspects of modern mathematics. https://en.wikipedia.org/wiki/Pierre_de_Fermat
|
||||
"fermat", |
||||
|
||||
// Enrico Fermi invented the first nuclear reactor. https://en.wikipedia.org/wiki/Enrico_Fermi.
|
||||
"fermi", |
||||
|
||||
// Richard Feynman was a key contributor to quantum mechanics and particle physics. https://en.wikipedia.org/wiki/Richard_Feynman
|
||||
"feynman", |
||||
|
||||
// Benjamin Franklin is famous for his experiments in electricity and the invention of the lightning rod.
|
||||
"franklin", |
||||
|
||||
// Galileo was a founding father of modern astronomy, and faced politics and obscurantism to establish scientific truth. https://en.wikipedia.org/wiki/Galileo_Galilei
|
||||
"galileo", |
||||
|
||||
// William Henry "Bill" Gates III is an American business magnate, philanthropist, investor, computer programmer, and inventor. https://en.wikipedia.org/wiki/Bill_Gates
|
||||
"gates", |
||||
|
||||
// Adele Goldberg, was one of the designers and developers of the Smalltalk language. https://en.wikipedia.org/wiki/Adele_Goldberg_(computer_scientist)
|
||||
"goldberg", |
||||
|
||||
// Adele Goldstine, born Adele Katz, wrote the complete technical description for the first electronic digital computer, ENIAC. https://en.wikipedia.org/wiki/Adele_Goldstine
|
||||
"goldstine", |
||||
|
||||
// Shafi Goldwasser is a computer scientist known for creating theoretical foundations of modern cryptography. Winner of 2012 ACM Turing Award. https://en.wikipedia.org/wiki/Shafi_Goldwasser
|
||||
"goldwasser", |
||||
|
||||
// James Golick, all around gangster.
|
||||
"golick", |
||||
|
||||
// Jane Goodall - British primatologist, ethologist, and anthropologist who is considered to be the world's foremost expert on chimpanzees - https://en.wikipedia.org/wiki/Jane_Goodall
|
||||
"goodall", |
||||
|
||||
// Lois Haibt - American computer scientist, part of the team at IBM that developed FORTRAN - https://en.wikipedia.org/wiki/Lois_Haibt
|
||||
"haibt", |
||||
|
||||
// Margaret Hamilton - Director of the Software Engineering Division of the MIT Instrumentation Laboratory, which developed on-board flight software for the Apollo space program. https://en.wikipedia.org/wiki/Margaret_Hamilton_(scientist)
|
||||
"hamilton", |
||||
|
||||
// Stephen Hawking pioneered the field of cosmology by combining general relativity and quantum mechanics. https://en.wikipedia.org/wiki/Stephen_Hawking
|
||||
"hawking", |
||||
|
||||
// Werner Heisenberg was a founding father of quantum mechanics. https://en.wikipedia.org/wiki/Werner_Heisenberg
|
||||
"heisenberg", |
||||
|
||||
// Grete Hermann was a German philosopher noted for her philosophical work on the foundations of quantum mechanics. https://en.wikipedia.org/wiki/Grete_Hermann
|
||||
"hermann", |
||||
|
||||
// Jaroslav Heyrovský was the inventor of the polarographic method, father of the electroanalytical method, and recipient of the Nobel Prize in 1959. His main field of work was polarography. https://en.wikipedia.org/wiki/Jaroslav_Heyrovsk%C3%BD
|
||||
"heyrovsky", |
||||
|
||||
// Dorothy Hodgkin was a British biochemist, credited with the development of protein crystallography. She was awarded the Nobel Prize in Chemistry in 1964. https://en.wikipedia.org/wiki/Dorothy_Hodgkin
|
||||
"hodgkin", |
||||
|
||||
// Erna Schneider Hoover revolutionized modern communication by inventing a computerized telephone switching method. https://en.wikipedia.org/wiki/Erna_Schneider_Hoover
|
||||
"hoover", |
||||
|
||||
// Grace Hopper developed the first compiler for a computer programming language and is credited with popularizing the term "debugging" for fixing computer glitches. https://en.wikipedia.org/wiki/Grace_Hopper
|
||||
"hopper", |
||||
|
||||
// Frances Hugle, she was an American scientist, engineer, and inventor who contributed to the understanding of semiconductors, integrated circuitry, and the unique electrical principles of microscopic materials. https://en.wikipedia.org/wiki/Frances_Hugle
|
||||
"hugle", |
||||
|
||||
// Hypatia - Greek Alexandrine Neoplatonist philosopher in Egypt who was one of the earliest mothers of mathematics - https://en.wikipedia.org/wiki/Hypatia
|
||||
"hypatia", |
||||
|
||||
// Yeong-Sil Jang was a Korean scientist and astronomer during the Joseon Dynasty; he invented the first metal printing press and water gauge. https://en.wikipedia.org/wiki/Jang_Yeong-sil
|
||||
"jang", |
||||
|
||||
// Betty Jennings - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Jean_Bartik
|
||||
"jennings", |
||||
|
||||
// Mary Lou Jepsen, was the founder and chief technology officer of One Laptop Per Child (OLPC), and the founder of Pixel Qi. https://en.wikipedia.org/wiki/Mary_Lou_Jepsen
|
||||
"jepsen", |
||||
|
||||
// Katherine Coleman Goble Johnson - American physicist and mathematician contributed to the NASA. https://en.wikipedia.org/wiki/Katherine_Johnson
|
||||
"johnson", |
||||
|
||||
// Irène Joliot-Curie - French scientist who was awarded the Nobel Prize for Chemistry in 1935. Daughter of Marie and Pierre Curie. https://en.wikipedia.org/wiki/Ir%C3%A8ne_Joliot-Curie
|
||||
"joliot", |
||||
|
||||
// Karen Spärck Jones came up with the concept of inverse document frequency, which is used in most search engines today. https://en.wikipedia.org/wiki/Karen_Sp%C3%A4rck_Jones
|
||||
"jones", |
||||
|
||||
// A. P. J. Abdul Kalam - is an Indian scientist aka Missile Man of India for his work on the development of ballistic missile and launch vehicle technology - https://en.wikipedia.org/wiki/A._P._J._Abdul_Kalam
|
||||
"kalam", |
||||
|
||||
// Susan Kare, created the icons and many of the interface elements for the original Apple Macintosh in the 1980s, and was an original employee of NeXT, working as the Creative Director. https://en.wikipedia.org/wiki/Susan_Kare
|
||||
"kare", |
||||
|
||||
// Mary Kenneth Keller, Sister Mary Kenneth Keller became the first American woman to earn a PhD in Computer Science in 1965. https://en.wikipedia.org/wiki/Mary_Kenneth_Keller
|
||||
"keller", |
||||
|
||||
// Har Gobind Khorana - Indian-American biochemist who shared the 1968 Nobel Prize for Physiology - https://en.wikipedia.org/wiki/Har_Gobind_Khorana
|
||||
"khorana", |
||||
|
||||
// Jack Kilby invented silicone integrated circuits and gave Silicon Valley its name. - https://en.wikipedia.org/wiki/Jack_Kilby
|
||||
"kilby", |
||||
|
||||
// Maria Kirch - German astronomer and first woman to discover a comet - https://en.wikipedia.org/wiki/Maria_Margarethe_Kirch
|
||||
"kirch", |
||||
|
||||
// Donald Knuth - American computer scientist, author of "The Art of Computer Programming" and creator of the TeX typesetting system. https://en.wikipedia.org/wiki/Donald_Knuth
|
||||
"knuth", |
||||
|
||||
// Sophie Kowalevski - Russian mathematician responsible for important original contributions to analysis, differential equations and mechanics - https://en.wikipedia.org/wiki/Sofia_Kovalevskaya
|
||||
"kowalevski", |
||||
|
||||
// Marie-Jeanne de Lalande - French astronomer, mathematician and cataloguer of stars - https://en.wikipedia.org/wiki/Marie-Jeanne_de_Lalande
|
||||
"lalande", |
||||
|
||||
// Hedy Lamarr - Actress and inventor. The principles of her work are now incorporated into modern Wi-Fi, CDMA and Bluetooth technology. https://en.wikipedia.org/wiki/Hedy_Lamarr
|
||||
"lamarr", |
||||
|
||||
// Leslie B. Lamport - American computer scientist. Lamport is best known for his seminal work in distributed systems and was the winner of the 2013 Turing Award. https://en.wikipedia.org/wiki/Leslie_Lamport
|
||||
"lamport", |
||||
|
||||
// Mary Leakey - British paleoanthropologist who discovered the first fossilized Proconsul skull - https://en.wikipedia.org/wiki/Mary_Leakey
|
||||
"leakey", |
||||
|
||||
// Henrietta Swan Leavitt - she was an American astronomer who discovered the relation between the luminosity and the period of Cepheid variable stars. https://en.wikipedia.org/wiki/Henrietta_Swan_Leavitt
|
||||
"leavitt", |
||||
|
||||
//Daniel Lewin - Mathematician, Akamai co-founder, soldier, 9/11 victim-- Developed optimization techniques for routing traffic on the internet. Died attempting to stop the 9-11 hijackers. https://en.wikipedia.org/wiki/Daniel_Lewin
|
||||
"lewin", |
||||
|
||||
// Ruth Lichterman - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Ruth_Teitelbaum
|
||||
"lichterman", |
||||
|
||||
// Barbara Liskov - co-developed the Liskov substitution principle. Liskov was also the winner of the Turing Prize in 2008. - https://en.wikipedia.org/wiki/Barbara_Liskov
|
||||
"liskov", |
||||
|
||||
// Ada Lovelace invented the first algorithm. https://en.wikipedia.org/wiki/Ada_Lovelace (thanks James Turnbull)
|
||||
"lovelace", |
||||
|
||||
// Auguste and Louis Lumière - the first filmmakers in history - https://en.wikipedia.org/wiki/Auguste_and_Louis_Lumi%C3%A8re
|
||||
"lumiere", |
||||
|
||||
// Mahavira - Ancient Indian mathematician during 9th century AD who discovered basic algebraic identities - https://en.wikipedia.org/wiki/Mah%C4%81v%C4%ABra_(mathematician)
|
||||
"mahavira", |
||||
|
||||
// Maria Mayer - American theoretical physicist and Nobel laureate in Physics for proposing the nuclear shell model of the atomic nucleus - https://en.wikipedia.org/wiki/Maria_Mayer
|
||||
"mayer", |
||||
|
||||
// John McCarthy invented LISP: https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)
|
||||
"mccarthy", |
||||
|
||||
// Barbara McClintock - a distinguished American cytogeneticist, 1983 Nobel Laureate in Physiology or Medicine for discovering transposons. https://en.wikipedia.org/wiki/Barbara_McClintock
|
||||
"mcclintock", |
||||
|
||||
// Malcolm McLean invented the modern shipping container: https://en.wikipedia.org/wiki/Malcom_McLean
|
||||
"mclean", |
||||
|
||||
// Kay McNulty - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Kathleen_Antonelli
|
||||
"mcnulty", |
||||
|
||||
// Lise Meitner - Austrian/Swedish physicist who was involved in the discovery of nuclear fission. The element meitnerium is named after her - https://en.wikipedia.org/wiki/Lise_Meitner
|
||||
"meitner", |
||||
|
||||
// Carla Meninsky, was the game designer and programmer for Atari 2600 games Dodge 'Em and Warlords. https://en.wikipedia.org/wiki/Carla_Meninsky
|
||||
"meninsky", |
||||
|
||||
// Johanna Mestorf - German prehistoric archaeologist and first female museum director in Germany - https://en.wikipedia.org/wiki/Johanna_Mestorf
|
||||
"mestorf", |
||||
|
||||
// Marvin Minsky - Pioneer in Artificial Intelligence, co-founder of the MIT's AI Lab, won the Turing Award in 1969. https://en.wikipedia.org/wiki/Marvin_Minsky
|
||||
"minsky", |
||||
|
||||
// Maryam Mirzakhani - an Iranian mathematician and the first woman to win the Fields Medal. https://en.wikipedia.org/wiki/Maryam_Mirzakhani
|
||||
"mirzakhani", |
||||
|
||||
// Samuel Morse - contributed to the invention of a single-wire telegraph system based on European telegraphs and was a co-developer of the Morse code - https://en.wikipedia.org/wiki/Samuel_Morse
|
||||
"morse", |
||||
|
||||
// Ian Murdock - founder of the Debian project - https://en.wikipedia.org/wiki/Ian_Murdock
|
||||
"murdock", |
||||
|
||||
// John von Neumann - todays computer architectures are based on the von Neumann architecture. https://en.wikipedia.org/wiki/Von_Neumann_architecture
|
||||
"neumann", |
||||
|
||||
// Isaac Newton invented classic mechanics and modern optics. https://en.wikipedia.org/wiki/Isaac_Newton
|
||||
"newton", |
||||
|
||||
// Florence Nightingale, more prominently known as a nurse, was also the first female member of the Royal Statistical Society and a pioneer in statistical graphics https://en.wikipedia.org/wiki/Florence_Nightingale#Statistics_and_sanitary_reform
|
||||
"nightingale", |
||||
|
||||
// Alfred Nobel - a Swedish chemist, engineer, innovator, and armaments manufacturer (inventor of dynamite) - https://en.wikipedia.org/wiki/Alfred_Nobel
|
||||
"nobel", |
||||
|
||||
// Emmy Noether, German mathematician. Noether's Theorem is named after her. https://en.wikipedia.org/wiki/Emmy_Noether
|
||||
"noether", |
||||
|
||||
// Poppy Northcutt. Poppy Northcutt was the first woman to work as part of NASA’s Mission Control. http://www.businessinsider.com/poppy-northcutt-helped-apollo-astronauts-2014-12?op=1
|
||||
"northcutt", |
||||
|
||||
// Robert Noyce invented silicone integrated circuits and gave Silicon Valley its name. - https://en.wikipedia.org/wiki/Robert_Noyce
|
||||
"noyce", |
||||
|
||||
// Panini - Ancient Indian linguist and grammarian from 4th century CE who worked on the world's first formal system - https://en.wikipedia.org/wiki/P%C4%81%E1%B9%87ini#Comparison_with_modern_formal_systems
|
||||
"panini", |
||||
|
||||
// Ambroise Pare invented modern surgery. https://en.wikipedia.org/wiki/Ambroise_Par%C3%A9
|
||||
"pare", |
||||
|
||||
// Louis Pasteur discovered vaccination, fermentation and pasteurization. https://en.wikipedia.org/wiki/Louis_Pasteur.
|
||||
"pasteur", |
||||
|
||||
// Cecilia Payne-Gaposchkin was an astronomer and astrophysicist who, in 1925, proposed in her Ph.D. thesis an explanation for the composition of stars in terms of the relative abundances of hydrogen and helium. https://en.wikipedia.org/wiki/Cecilia_Payne-Gaposchkin
|
||||
"payne", |
||||
|
||||
// Radia Perlman is a software designer and network engineer and most famous for her invention of the spanning-tree protocol (STP). https://en.wikipedia.org/wiki/Radia_Perlman
|
||||
"perlman", |
||||
|
||||
// Rob Pike was a key contributor to Unix, Plan 9, the X graphic system, utf-8, and the Go programming language. https://en.wikipedia.org/wiki/Rob_Pike
|
||||
"pike", |
||||
|
||||
// Henri Poincaré made fundamental contributions in several fields of mathematics. https://en.wikipedia.org/wiki/Henri_Poincar%C3%A9
|
||||
"poincare", |
||||
|
||||
// Laura Poitras is a director and producer whose work, made possible by open source crypto tools, advances the causes of truth and freedom of information by reporting disclosures by whistleblowers such as Edward Snowden. https://en.wikipedia.org/wiki/Laura_Poitras
|
||||
"poitras", |
||||
|
||||
// Claudius Ptolemy - a Greco-Egyptian writer of Alexandria, known as a mathematician, astronomer, geographer, astrologer, and poet of a single epigram in the Greek Anthology - https://en.wikipedia.org/wiki/Ptolemy
|
||||
"ptolemy", |
||||
|
||||
// C. V. Raman - Indian physicist who won the Nobel Prize in 1930 for proposing the Raman effect. - https://en.wikipedia.org/wiki/C._V._Raman
|
||||
"raman", |
||||
|
||||
// Srinivasa Ramanujan - Indian mathematician and autodidact who made extraordinary contributions to mathematical analysis, number theory, infinite series, and continued fractions. - https://en.wikipedia.org/wiki/Srinivasa_Ramanujan
|
||||
"ramanujan", |
||||
|
||||
// Sally Kristen Ride was an American physicist and astronaut. She was the first American woman in space, and the youngest American astronaut. https://en.wikipedia.org/wiki/Sally_Ride
|
||||
"ride", |
||||
|
||||
// Rita Levi-Montalcini - Won Nobel Prize in Physiology or Medicine jointly with colleague Stanley Cohen for the discovery of nerve growth factor (https://en.wikipedia.org/wiki/Rita_Levi-Montalcini)
|
||||
"montalcini", |
||||
|
||||
// Dennis Ritchie - co-creator of UNIX and the C programming language. - https://en.wikipedia.org/wiki/Dennis_Ritchie
|
||||
"ritchie", |
||||
|
||||
// Wilhelm Conrad Röntgen - German physicist who was awarded the first Nobel Prize in Physics in 1901 for the discovery of X-rays (Röntgen rays). https://en.wikipedia.org/wiki/Wilhelm_R%C3%B6ntgen
|
||||
"roentgen", |
||||
|
||||
// Rosalind Franklin - British biophysicist and X-ray crystallographer whose research was critical to the understanding of DNA - https://en.wikipedia.org/wiki/Rosalind_Franklin
|
||||
"rosalind", |
||||
|
||||
// Meghnad Saha - Indian astrophysicist best known for his development of the Saha equation, used to describe chemical and physical conditions in stars - https://en.wikipedia.org/wiki/Meghnad_Saha
|
||||
"saha", |
||||
|
||||
// Jean E. Sammet developed FORMAC, the first widely used computer language for symbolic manipulation of mathematical formulas. https://en.wikipedia.org/wiki/Jean_E._Sammet
|
||||
"sammet", |
||||
|
||||
// Carol Shaw - Originally an Atari employee, Carol Shaw is said to be the first female video game designer. https://en.wikipedia.org/wiki/Carol_Shaw_(video_game_designer)
|
||||
"shaw", |
||||
|
||||
// Dame Stephanie "Steve" Shirley - Founded a software company in 1962 employing women working from home. https://en.wikipedia.org/wiki/Steve_Shirley
|
||||
"shirley", |
||||
|
||||
// William Shockley co-invented the transistor - https://en.wikipedia.org/wiki/William_Shockley
|
||||
"shockley", |
||||
|
||||
// Françoise Barré-Sinoussi - French virologist and Nobel Prize Laureate in Physiology or Medicine; her work was fundamental in identifying HIV as the cause of AIDS. https://en.wikipedia.org/wiki/Fran%C3%A7oise_Barr%C3%A9-Sinoussi
|
||||
"sinoussi", |
||||
|
||||
// Betty Snyder - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Betty_Holberton
|
||||
"snyder", |
||||
|
||||
// Frances Spence - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Frances_Spence
|
||||
"spence", |
||||
|
||||
// Richard Matthew Stallman - the founder of the Free Software movement, the GNU project, the Free Software Foundation, and the League for Programming Freedom. He also invented the concept of copyleft to protect the ideals of this movement, and enshrined this concept in the widely-used GPL (General Public License) for software. https://en.wikiquote.org/wiki/Richard_Stallman
|
||||
"stallman", |
||||
|
||||
// Michael Stonebraker is a database research pioneer and architect of Ingres, Postgres, VoltDB and SciDB. Winner of 2014 ACM Turing Award. https://en.wikipedia.org/wiki/Michael_Stonebraker
|
||||
"stonebraker", |
||||
|
||||
// Janese Swanson (with others) developed the first of the Carmen Sandiego games. She went on to found Girl Tech. https://en.wikipedia.org/wiki/Janese_Swanson
|
||||
"swanson", |
||||
|
||||
// Aaron Swartz was influential in creating RSS, Markdown, Creative Commons, Reddit, and much of the internet as we know it today. He was devoted to freedom of information on the web. https://en.wikiquote.org/wiki/Aaron_Swartz
|
||||
"swartz", |
||||
|
||||
// Bertha Swirles was a theoretical physicist who made a number of contributions to early quantum theory. https://en.wikipedia.org/wiki/Bertha_Swirles
|
||||
"swirles", |
||||
|
||||
// Nikola Tesla invented the AC electric system and every gadget ever used by a James Bond villain. https://en.wikipedia.org/wiki/Nikola_Tesla
|
||||
"tesla", |
||||
|
||||
// Ken Thompson - co-creator of UNIX and the C programming language - https://en.wikipedia.org/wiki/Ken_Thompson
|
||||
"thompson", |
||||
|
||||
// Linus Torvalds invented Linux and Git. https://en.wikipedia.org/wiki/Linus_Torvalds
|
||||
"torvalds", |
||||
|
||||
// Alan Turing was a founding father of computer science. https://en.wikipedia.org/wiki/Alan_Turing.
|
||||
"turing", |
||||
|
||||
// Varahamihira - Ancient Indian mathematician who discovered trigonometric formulae during 505-587 CE - https://en.wikipedia.org/wiki/Var%C4%81hamihira#Contributions
|
||||
"varahamihira", |
||||
|
||||
// Sir Mokshagundam Visvesvaraya - is a notable Indian engineer. He is a recipient of the Indian Republic's highest honour, the Bharat Ratna, in 1955. On his birthday, 15 September is celebrated as Engineer's Day in India in his memory - https://en.wikipedia.org/wiki/Visvesvaraya
|
||||
"visvesvaraya", |
||||
|
||||
// Christiane Nüsslein-Volhard - German biologist, won Nobel Prize in Physiology or Medicine in 1995 for research on the genetic control of embryonic development. https://en.wikipedia.org/wiki/Christiane_N%C3%BCsslein-Volhard
|
||||
"volhard", |
||||
|
||||
// Marlyn Wescoff - one of the original programmers of the ENIAC. https://en.wikipedia.org/wiki/ENIAC - https://en.wikipedia.org/wiki/Marlyn_Meltzer
|
||||
"wescoff", |
||||
|
||||
// Andrew Wiles - Notable British mathematician who proved the enigmatic Fermat's Last Theorem - https://en.wikipedia.org/wiki/Andrew_Wiles
|
||||
"wiles", |
||||
|
||||
// Roberta Williams, did pioneering work in graphical adventure games for personal computers, particularly the King's Quest series. https://en.wikipedia.org/wiki/Roberta_Williams
|
||||
"williams", |
||||
|
||||
// Sophie Wilson designed the first Acorn Micro-Computer and the instruction set for ARM processors. https://en.wikipedia.org/wiki/Sophie_Wilson
|
||||
"wilson", |
||||
|
||||
// Jeannette Wing - co-developed the Liskov substitution principle. - https://en.wikipedia.org/wiki/Jeannette_Wing
|
||||
"wing", |
||||
|
||||
// Steve Wozniak invented the Apple I and Apple II. https://en.wikipedia.org/wiki/Steve_Wozniak
|
||||
"wozniak", |
||||
|
||||
// The Wright brothers, Orville and Wilbur - credited with inventing and building the world's first successful airplane and making the first controlled, powered and sustained heavier-than-air human flight - https://en.wikipedia.org/wiki/Wright_brothers
|
||||
"wright", |
||||
|
||||
// Rosalyn Sussman Yalow - Rosalyn Sussman Yalow was an American medical physicist, and a co-winner of the 1977 Nobel Prize in Physiology or Medicine for development of the radioimmunoassay technique. https://en.wikipedia.org/wiki/Rosalyn_Sussman_Yalow
|
||||
"yalow", |
||||
|
||||
// Ada Yonath - an Israeli crystallographer, the first woman from the Middle East to win a Nobel prize in the sciences. https://en.wikipedia.org/wiki/Ada_Yonath
|
||||
"yonath", |
||||
|
||||
// Misc names that are fun to add including bands and musicians I like.
|
||||
|
||||
// Trent Reznor
|
||||
"reznor", |
||||
|
||||
// Jennifer Parkin
|
||||
"ayria", |
||||
|
||||
// https://en.wikipedia.org/wiki/Iris_(American_band)
|
||||
"iris", |
||||
|
||||
// https://theprodigy.com/
|
||||
"prodigy", |
||||
|
||||
// https://en.wikipedia.org/wiki/Rush_(band)
|
||||
"rush", |
||||
|
||||
// Animal Crossing characters that aren't human names
|
||||
"barold", "nook", "zucker", "cherry", "cookie", "beardo", "deli", |
||||
|
||||
// Matrix character names
|
||||
"trinity", "neo", "apoc", "dozer", "morpheus", "tank", "switch", |
||||
|
||||
// Random fun nouns
|
||||
"multipass", "pizza", "dna", |
||||
|
||||
// Video game characters
|
||||
"mario", "zelda", "link", |
||||
|
||||
// Ultimate frisbee terminology for Ginger
|
||||
"huck", "hammer", "scoober", "disc", "frisbee", |
||||
} |
||||
) |
||||
|
||||
func GeneratePhrase() string { |
||||
r := rand.New(rand.NewSource(time.Now().UnixNano())) //nolint
|
||||
|
||||
left_index := int(r.Float32() * float32(len(left))) |
||||
right_index := int(r.Float32() * float32(len(right))) |
||||
|
||||
return fmt.Sprintf("%s-%s", left[left_index], right[right_index]) |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import {URL_CHAT_REGISTRATION} from "../utils/constants.js"; |
||||
|
||||
export async function registerChat(username) { |
||||
const options = { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json' |
||||
}, |
||||
body: JSON.stringify({displayName: username}) |
||||
} |
||||
|
||||
try { |
||||
const response = await fetch(URL_CHAT_REGISTRATION, options); |
||||
const result = await response.json(); |
||||
return result; |
||||
} catch(e) { |
||||
console.error(e); |
||||
} |
||||
} |
@ -1,31 +1,17 @@
@@ -1,31 +1,17 @@
|
||||
export function messageBubbleColorForString(str) { |
||||
let hash = 0; |
||||
for (let i = 0; i < str.length; i++) { |
||||
// eslint-disable-next-line
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash); |
||||
} |
||||
|
||||
export function messageBubbleColorForHue(hue) { |
||||
// Tweak these to adjust the result of the color
|
||||
const saturation = 25; |
||||
const lightness = 45; |
||||
const saturation = 45; |
||||
const lightness = 50; |
||||
const alpha = 'var(--message-background-alpha)'; |
||||
const hue = parseInt(Math.abs(hash), 16) % 360; |
||||
|
||||
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; |
||||
} |
||||
|
||||
export function textColorForString(str) { |
||||
let hash = 0; |
||||
for (let i = 0; i < str.length; i++) { |
||||
// eslint-disable-next-line
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash); |
||||
} |
||||
|
||||
export function textColorForHue(hue) { |
||||
// Tweak these to adjust the result of the color
|
||||
const saturation = 80; |
||||
const lightness = 80; |
||||
const alpha = 0.8; |
||||
const hue = parseInt(Math.abs(hash), 16) % 360; |
||||
|
||||
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; |
||||
} |
||||
|
Loading…
Reference in new issue