commit
ca80d96b37
9 changed files with 202 additions and 16 deletions
|
@ -3,6 +3,10 @@
|
|||
### What is this?
|
||||
Justlog is an twitch irc bot. It focuses on logging and providing an api for the logs.
|
||||
|
||||
### Optout
|
||||
|
||||
Click the X icon on the web ui to find a explanation how to opt out.
|
||||
|
||||
### API
|
||||
|
||||
API documentation can be viewed via the justlog frontend by clicking the "docs" symbol:
|
||||
|
|
45
api/optout.go
Normal file
45
api/optout.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
}
|
87
web/src/components/Optout.tsx
Normal file
87
web/src/components/Optout.tsx
Normal 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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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 };
|
||||
|
|
Loading…
Add table
Reference in a new issue