allow opting out for everyone

This commit is contained in:
gempir 2022-07-31 15:03:21 +02:00
parent af629b1e81
commit f73c8c073e
9 changed files with 198 additions and 16 deletions

45
api/optout.go Normal file
View file

@ -0,0 +1,45 @@
package api
import (
"math/rand"
"net/http"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// swagger:route POST /optout justlog
//
// Generates optout code to use in chat
//
// Produces:
// - application/json
//
// Schemes: https
//
// Responses:
// 200: string
func (s *Server) writeOptOutCode(w http.ResponseWriter, r *http.Request) {
code := randomString(6)
s.bot.OptoutCodes.Store(code, true)
go func() {
time.Sleep(time.Second * 60)
s.bot.OptoutCodes.Delete(code)
}()
writeJSON(code, http.StatusOK, w, r)
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
func randomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}

View file

@ -131,6 +131,11 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) {
return
}
if url == "/optout" && r.Method == http.MethodPost {
s.writeOptOutCode(w, r)
return
}
if strings.HasPrefix(url, "/admin/channels") {
success := s.authenticateAdmin(w, r)
if success {
@ -195,11 +200,11 @@ func (s *Server) routeLogs(w http.ResponseWriter, r *http.Request) bool {
var logs *chatLog
if request.time.random {
if request.isUserRequest {
logs, err = s.getRandomQuote(request)
} else {
logs, err = s.getChannelRandomQuote(request)
}
if request.isUserRequest {
logs, err = s.getRandomQuote(request)
} else {
logs, err = s.getChannelRandomQuote(request)
}
} else if request.time.from != "" && request.time.to != "" {
if request.isUserRequest {
logs, err = s.getUserLogsRange(request)

View file

@ -20,10 +20,6 @@ func (b *Bot) handlePrivateMessageCommands(message twitch.PrivateMessage) {
return
}
if !contains(b.cfg.Admins, message.User.Name) {
return
}
args := strings.Fields(message.Message[len(commandPrefix):])
if len(args) < 1 {
return
@ -33,19 +29,30 @@ func (b *Bot) handlePrivateMessageCommands(message twitch.PrivateMessage) {
switch commandName {
case "status":
if !contains(b.cfg.Admins, message.User.Name) {
return
}
uptime := humanize.TimeSince(b.startTime)
b.Say(message.Channel, fmt.Sprintf("%s, uptime: %s", message.User.DisplayName, uptime))
case "join":
if !contains(b.cfg.Admins, message.User.Name) {
return
}
b.handleJoin(message, args)
case "part":
if !contains(b.cfg.Admins, message.User.Name) {
return
}
b.handlePart(message, args)
case "optout":
b.handleOptOut(message, args)
case "optin":
if !contains(b.cfg.Admins, message.User.Name) {
return
}
b.handleOptIn(message, args)
}
}
@ -100,6 +107,16 @@ func (b *Bot) handleOptOut(message twitch.PrivateMessage, args []string) {
return
}
if _, ok := b.OptoutCodes.LoadAndDelete(args[0]); ok {
b.cfg.OptOutUsers(message.User.ID)
b.Say(message.Channel, fmt.Sprintf("%s, opted you out", message.User.DisplayName))
return
}
if !contains(b.cfg.Admins, message.User.Name) {
return
}
users, err := b.helixClient.GetUsersByUsernames(args)
if err != nil {
log.Error(err)

View file

@ -23,6 +23,7 @@ type Bot struct {
worker []*worker
channels map[string]helix.UserData
clearchats sync.Map
OptoutCodes sync.Map
}
type worker struct {
@ -43,6 +44,7 @@ func NewBot(cfg *config.Config, helixClient helix.TwitchApiClient, fileLogger *f
fileLogger: fileLogger,
channels: channels,
worker: []*worker{},
OptoutCodes: sync.Map{},
}
}

View file

View file

@ -6,6 +6,7 @@ import styled from "styled-components";
import { useChannels } from "../hooks/useChannels";
import { store } from "../store";
import { Docs } from "./Docs";
import { Optout } from "./Optout";
import { Settings } from "./Settings";
const FiltersContainer = styled.form`
@ -66,6 +67,7 @@ export function Filters() {
<Button variant="contained" color="primary" size="large" type="submit">load</Button>
<Settings />
<Docs />
<Optout />
</FiltersContainer>
</FiltersWrapper>
}

View file

@ -0,0 +1,87 @@
import { IconButton, Button } from "@material-ui/core";
import { useContext, useState } from "react";
import styled from "styled-components";
import { store } from "../store";
import CancelIcon from '@material-ui/icons/Cancel';
const OptoutWrapper = styled.div`
`;
export function Optout() {
const { state, setShowOptout } = useContext(store);
const handleClick = () => {
setShowOptout(!state.showOptout);
}
return <OptoutWrapper>
<IconButton aria-controls="docs" aria-haspopup="true" onClick={handleClick} size="small" color={state.showOptout ? "primary" : "default"}>
<CancelIcon />
</IconButton>
</OptoutWrapper>;
}
const OptoutPanelWrapper = styled.div`
background: var(--bg-bright);
color: var(--text);
margin: 3rem;
font-size: 1.5rem;
padding: 2rem;
code {
background: var(--bg);
padding: 1rem;
border-radius: 3px;
}
.generator {
margin-top: 2rem;
display: flex;
gap: 1rem;
align-items: center;
input {
background: var(--bg);
border: none;
color: white;
padding: 0.6rem;
font-size: 1.5rem;
text-align: center;
border-radius: 3px;
}
}
.small {
font-size: 0.8rem;
font-family: monospace;
}
`;
export function OptoutPanel() {
const { state } = useContext(store);
const [code, setCode] = useState("");
const generateCode = () => {
fetch(state.apiBaseUrl + "/optout", { method: "POST" }).then(res => res.json()).then(setCode).catch(console.error);
};
return <OptoutPanelWrapper>
<p>
You can opt out from being logged. This will also disable access to your previously logged data.<br />
Opting out is permanent, there is not reverse action. So think twice if you want to opt out.
</p>
<p>
If you still want to optout generate a token here and paste the command into a logged chat.<br />
You will receive a confirmation message from the bot "@username, opted you out".
</p>
<br />
<div><code>!justlog optout {"<code>"}</code></div>
<div className="generator">
<input readOnly type="text" value={code} /><Button variant="contained" onClick={generateCode} color="primary" size="large">Generate Code</Button>
</div>
{code && <p className="small">
This code is valid for 60 seconds
</p>}
</OptoutPanelWrapper>;
}

View file

@ -1,15 +1,20 @@
import React from "react";
import React, { useContext } from "react";
import styled from "styled-components";
import { store } from "../store";
import { Filters } from "./Filters";
import { LogContainer } from "./LogContainer";
import { OptoutPanel } from "./Optout";
const PageContainer = styled.div`
`;
export function Page() {
const {state} = useContext(store);
return <PageContainer>
<Filters />
{state.showOptout && <OptoutPanel />}
<LogContainer />
</PageContainer>;
}

View file

@ -1,5 +1,5 @@
import React, { createContext, useState } from "react";
import { QueryClient } from 'react-query'
import { createContext, useState } from "react";
import { QueryClient } from 'react-query';
import { useLocalStorage } from "./hooks/useLocalStorage";
export interface Settings {
@ -32,6 +32,7 @@ export interface State {
error: boolean,
activeSearchField: HTMLInputElement | null,
showSwagger: boolean,
showOptout: boolean,
}
export type Action = Record<string, unknown>;
@ -40,7 +41,7 @@ const url = new URL(window.location.href);
const defaultContext = {
state: {
queryClient: new QueryClient(),
apiBaseUrl: process.env.REACT_APP_API_BASE_URL ?? window.location.protocol + "//" + window.location.host,
apiBaseUrl: process?.env.REACT_APP_API_BASE_URL ?? window.location.protocol + "//" + window.location.host,
settings: {
showEmotes: {
displayName: "Show Emotes",
@ -66,12 +67,14 @@ const defaultContext = {
currentChannel: url.searchParams.get("channel"),
currentUsername: url.searchParams.get("username"),
showSwagger: url.searchParams.has("swagger"),
showOptout: url.searchParams.has("optout"),
error: false,
} as State,
setState: (state: State) => { },
setCurrents: (currentChannel: string | null = null, currentUsername: string | null = null) => { },
setSettings: (newSettings: Settings) => { },
setShowSwagger: (show: boolean) => { },
setShowOptout: (show: boolean) => { },
};
const store = createContext(defaultContext);
@ -87,13 +90,29 @@ const StateProvider = ({ children }: { children: JSX.Element }): JSX.Element =>
if (show) {
url.searchParams.set("swagger", "")
url.searchParams.delete("optout");
} else {
url.searchParams.delete("swagger");
}
window.history.replaceState({}, "justlog", url.toString());
setState({ ...state, showSwagger: show })
setState({ ...state, showSwagger: show, showOptout: false })
}
const setShowOptout = (show: boolean) => {
const url = new URL(window.location.href);
if (show) {
url.searchParams.set("optout", "");
url.searchParams.delete("swagger");
} else {
url.searchParams.delete("optout");
}
window.history.replaceState({}, "justlog", url.toString());
setState({ ...state, showOptout: show, showSwagger: false })
}
const setSettings = (newSettings: Settings) => {
@ -126,7 +145,7 @@ const StateProvider = ({ children }: { children: JSX.Element }): JSX.Element =>
window.history.replaceState({}, "justlog", url.toString());
}
return <Provider value={{ state, setState, setSettings, setCurrents, setShowSwagger }}>{children}</Provider>;
return <Provider value={{ state, setState, setSettings, setCurrents, setShowSwagger, setShowOptout }}>{children}</Provider>;
};
export { store, StateProvider };