Added chatbot functionality

This commit is contained in:
tyler 2024-05-02 15:30:25 -04:00
parent 79031a1978
commit 2906a372d5
37 changed files with 3274 additions and 175 deletions

View file

@ -1,9 +1,18 @@
# Doing # Doing
Next steps: Next steps:
- add option to delete bot rules
- delete page needs to handle new architecture - delete page needs to handle new architecture
- app.producers.Active(*name) instead of app.producers.ApiP.Active(*name)
- app.producers.Stop(*name)
- activatePage: verify defer page.activeMu.Unlock does not conflict with display function - activatePage: verify defer page.activeMu.Unlock does not conflict with display function
For Dashboard page,
- Api or chat producer could error, need to be able to start/restart both handlers on error
- Show user error if api or chat stop/error
On API errors On API errors
- include backoff multiple, if exceeded then stop API - include backoff multiple, if exceeded then stop API

259
v1/app.go
View file

@ -40,10 +40,13 @@ type Page struct {
name string name string
} }
func (p *Page) staticLiveStreamUrl() string {
return fmt.Sprintf("https://rumble.com%s/live", p.name)
}
// App struct // App struct
type App struct { type App struct {
//apiS *ApiService cancelProc context.CancelFunc
cancelCtrl context.CancelFunc
clients map[string]*rumblelivestreamlib.Client clients map[string]*rumblelivestreamlib.Client
clientsMu sync.Mutex clientsMu sync.Mutex
displaying string displaying string
@ -70,8 +73,6 @@ func NewApp() *App {
log.Fatal("error initializing log: ", err) log.Fatal("error initializing log: ", err)
} }
//app.apiS = NewApiService(app.logError, app.logInfo)
return app return app
} }
@ -94,30 +95,28 @@ func (a *App) log() error {
// so we can call the runtime methods // so we can call the runtime methods
func (a *App) startup(wails context.Context) { func (a *App) startup(wails context.Context) {
a.wails = wails a.wails = wails
//a.apiS.Startup(a.apiS.ch)
// ctx, cancel := context.WithCancel(context.Background())
// a.cancelCtrl = cancel
// go a.handle(ctx)
} }
func (a *App) handle(ctx context.Context) { func (a *App) process(ctx context.Context) {
for { for {
select { select {
case apiE := <-a.producers.ApiP.Ch: case apiE := <-a.producers.ApiP.Ch:
err := a.handleApi(apiE) err := a.processApi(apiE)
if err != nil { if err != nil {
a.logError.Println("error handling API event:", err) a.logError.Println("error handling API event:", err)
} }
case chatE := <-a.producers.ChatP.Ch:
err := a.processChat(chatE)
if err != nil {
a.logError.Println("error handling chat event:", err)
}
case <-ctx.Done(): case <-ctx.Done():
return return
} }
} }
} }
func (a *App) handleApi(event events.Api) error { func (a *App) processApi(event events.Api) error {
if event.Name == "" { if event.Name == "" {
return fmt.Errorf("event name is empty") return fmt.Errorf("event name is empty")
} }
@ -134,7 +133,7 @@ func (a *App) handleApi(event events.Api) error {
} }
page.apiSt.activeMu.Lock() page.apiSt.activeMu.Lock()
page.apiSt.active = event.Stop page.apiSt.active = !event.Stop
page.apiSt.activeMu.Unlock() page.apiSt.activeMu.Unlock()
if event.Stop { if event.Stop {
@ -158,21 +157,17 @@ func (a *App) handleApi(event events.Api) error {
return nil return nil
} }
func (a *App) shutdown(ctx context.Context) { func (a *App) processChat(event events.Chat) error {
// if a.apiS != nil && a.apiS.Api != nil { return nil
// err := a.apiS.Shutdown() }
// if err != nil {
// a.logError.Println("error shutting down api:", err)
// }
// close(a.apiS.ch) func (a *App) shutdown(ctx context.Context) {
// }
err := a.producers.Shutdown() err := a.producers.Shutdown()
if err != nil { if err != nil {
a.logError.Println("error closing event producers:", err) a.logError.Println("error closing event producers:", err)
} }
a.cancelCtrl() a.cancelProc()
if a.services != nil { if a.services != nil {
err := a.services.Close() err := a.services.Close()
@ -223,8 +218,8 @@ func (a *App) Start() (bool, error) {
// runtime.EventsEmit(a.ctx, "StartupMessage", "Checking for updates complete.") // runtime.EventsEmit(a.ctx, "StartupMessage", "Checking for updates complete.")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
a.cancelCtrl = cancel a.cancelProc = cancel
go a.handle(ctx) go a.process(ctx)
signin := true signin := true
if count > 0 { if count > 0 {
@ -238,6 +233,7 @@ func (a *App) initProducers() error {
producers, err := events.NewProducers( producers, err := events.NewProducers(
events.WithLoggers(a.logError, a.logInfo), events.WithLoggers(a.logError, a.logInfo),
events.WithApiProducer(), events.WithApiProducer(),
events.WithChatProducer(),
) )
if err != nil { if err != nil {
return fmt.Errorf("error initializing producers: %v", err) return fmt.Errorf("error initializing producers: %v", err)
@ -264,6 +260,7 @@ func (a *App) initServices() error {
models.WithAccountService(), models.WithAccountService(),
models.WithChannelService(), models.WithChannelService(),
models.WithAccountChannelService(), models.WithAccountChannelService(),
models.WithChatbotService(),
) )
if err != nil { if err != nil {
return fmt.Errorf("error initializing services: %v", err) return fmt.Errorf("error initializing services: %v", err)
@ -310,7 +307,9 @@ func (a *App) verifyAccounts() (int, error) {
} else { } else {
account.Cookies = nil account.Cookies = nil
err = a.services.AccountS.Update(&account) err = a.services.AccountS.Update(&account)
fmt.Errorf("error updating account: %v", err) if err != nil {
return -1, fmt.Errorf("error updating account: %v", err)
}
} }
} }
} }
@ -319,7 +318,7 @@ func (a *App) verifyAccounts() (int, error) {
} }
func (a *App) AddPage(apiKey string) error { func (a *App) AddPage(apiKey string) error {
client := rumblelivestreamlib.Client{StreamKey: apiKey} client := rumblelivestreamlib.Client{ApiKey: apiKey}
resp, err := client.Request() resp, err := client.Request()
if err != nil { if err != nil {
a.logError.Println("error executing api request:", err) a.logError.Println("error executing api request:", err)
@ -347,6 +346,13 @@ func (a *App) AddPage(apiKey string) error {
} }
} }
list, err := a.accountList()
if err != nil {
a.logError.Println("error getting account list:", err)
return fmt.Errorf("Error logging in. Try again.")
}
runtime.EventsEmit(a.wails, "PageSideBarAccounts", list)
return nil return nil
} }
@ -833,7 +839,6 @@ func (a *App) DeleteAccount(id int64) error {
a.logError.Println("error getting account list:", err) a.logError.Println("error getting account list:", err)
return fmt.Errorf("Error deleting account. Try again.") return fmt.Errorf("Error deleting account. Try again.")
} }
runtime.EventsEmit(a.wails, "PageSideBarAccounts", list) runtime.EventsEmit(a.wails, "PageSideBarAccounts", list)
return nil return nil
@ -876,7 +881,6 @@ func (a *App) DeleteChannel(id int64) error {
a.logError.Println("error getting account list:", err) a.logError.Println("error getting account list:", err)
return fmt.Errorf("Error deleting channel. Try again.") return fmt.Errorf("Error deleting channel. Try again.")
} }
runtime.EventsEmit(a.wails, "PageSideBarAccounts", list) runtime.EventsEmit(a.wails, "PageSideBarAccounts", list)
return nil return nil
@ -958,7 +962,6 @@ func (a *App) PageStatus(name string) {
active := false active := false
isLive := false isLive := false
// resp := a.api.Response(name)
a.pagesMu.Lock() a.pagesMu.Lock()
defer a.pagesMu.Unlock() defer a.pagesMu.Unlock()
page, exists := a.pages[name] page, exists := a.pages[name]
@ -1002,7 +1005,7 @@ func (a *App) UpdateAccountApi(id int64, apiKey string) error {
} }
} }
client := rumblelivestreamlib.Client{StreamKey: apiKey} client := rumblelivestreamlib.Client{ApiKey: apiKey}
resp, err := client.Request() resp, err := client.Request()
if err != nil { if err != nil {
a.logError.Println("error executing api request:", err) a.logError.Println("error executing api request:", err)
@ -1047,7 +1050,7 @@ func (a *App) UpdateChannelApi(id int64, apiKey string) error {
} }
} }
client := rumblelivestreamlib.Client{StreamKey: apiKey} client := rumblelivestreamlib.Client{ApiKey: apiKey}
resp, err := client.Request() resp, err := client.Request()
if err != nil { if err != nil {
a.logError.Println("error executing api request:", err) a.logError.Println("error executing api request:", err)
@ -1067,3 +1070,193 @@ func (a *App) UpdateChannelApi(id int64, apiKey string) error {
return nil return nil
} }
func (a *App) DeleteChatbot(chatbot *models.Chatbot) error {
if chatbot == nil || chatbot.ID == nil {
return fmt.Errorf("Invalid chatbot. Try again.")
}
cb, err := a.services.ChatbotS.ByID(*chatbot.ID)
if err != nil {
a.logError.Println("error getting chatbot by ID:", err)
return fmt.Errorf("Error deleting chatbot. Try again.")
}
if cb == nil {
return fmt.Errorf("Chatbot does not exist.")
}
err = a.services.ChatbotS.Delete(chatbot)
if err != nil {
a.logError.Println("error deleting chatbot:", err)
return fmt.Errorf("Error deleting chatbot. Try again.")
}
list, err := a.chatbotList()
if err != nil {
a.logError.Println("error getting chatbot list:", err)
return fmt.Errorf("Error deleting chatbot. Try again.")
}
runtime.EventsEmit(a.wails, "ChatbotList", list)
return nil
}
func (a *App) NewChatbot(chatbot *models.Chatbot) error {
if chatbot == nil || chatbot.Name == nil {
return fmt.Errorf("Invalid chatbot. Try again.")
}
cb, err := a.services.ChatbotS.ByName(*chatbot.Name)
if err != nil {
a.logError.Println("error getting chatbot by name:", err)
return fmt.Errorf("Error creating chatbot. Try again.")
}
if cb != nil {
return fmt.Errorf("Chatbot name already exists.")
}
_, err = a.services.ChatbotS.Create(chatbot)
if err != nil {
a.logError.Println("error creating chatbot:", err)
return fmt.Errorf("Error creating chatbot. Try again.")
}
list, err := a.chatbotList()
if err != nil {
a.logError.Println("error getting chatbot list:", err)
return fmt.Errorf("Error creating chatbot. Try again.")
}
runtime.EventsEmit(a.wails, "ChatbotList", list)
return nil
}
func (a *App) UpdateChatbot(chatbot *models.Chatbot) error {
if chatbot == nil || chatbot.ID == nil || chatbot.Name == nil {
return fmt.Errorf("Invalid chatbot. Try again.")
}
cb, err := a.services.ChatbotS.ByID(*chatbot.ID)
if err != nil {
a.logError.Println("error getting chatbot by ID:", err)
return fmt.Errorf("Error updating chatbot. Try again.")
}
if cb == nil {
return fmt.Errorf("Chatbot does not exist.")
}
cbByName, err := a.services.ChatbotS.ByName(*chatbot.Name)
if err != nil {
a.logError.Println("error getting chatbot by Name:", err)
return fmt.Errorf("Error updating chatbot. Try again.")
}
if cbByName != nil && *cbByName.ID != *cb.ID {
return fmt.Errorf("Chatbot name already exists.")
}
err = a.services.ChatbotS.Update(chatbot)
if err != nil {
a.logError.Println("error updating chatbot:", err)
return fmt.Errorf("Error updating chatbot. Try again.")
}
list, err := a.chatbotList()
if err != nil {
a.logError.Println("error getting chatbot list:", err)
return fmt.Errorf("Error updating chatbot. Try again.")
}
runtime.EventsEmit(a.wails, "ChatbotList", list)
return nil
}
func (a *App) ChatbotList() ([]models.Chatbot, error) {
list, err := a.chatbotList()
if err != nil {
a.logError.Println("error getting chatbot list:", err)
return nil, fmt.Errorf("Error retrieving chatbots. Try restarting.")
}
return list, nil
}
func (a *App) chatbotList() ([]models.Chatbot, error) {
list, err := a.services.ChatbotS.All()
if err != nil {
return nil, fmt.Errorf("error querying all chatbots: %v", err)
}
return list, err
}
type ChatbotRule struct {
Message *ChatbotRuleMessage `json:"message"`
SendAs *ChatbotRuleSender `json:"send_as"`
Trigger *ChatbotRuleTrigger `json:"trigger"`
}
type ChatbotRuleMessage struct {
FromFile *ChatbotRuleMessageFile `json:"from_file"`
FromText string `json:"from_text"`
}
type ChatbotRuleMessageFile struct {
Filepath string `json:"filepath"`
RandomRead bool `json:"random_read"`
}
type ChatbotRuleSender struct {
Username string `json:"username"`
ChannelID *int `json:"channel_id"`
}
type ChatbotRuleTrigger struct {
OnCommand *ChatbotRuleTriggerCommand `json:"on_command"`
OnEvent *ChatbotRuleTriggerEvent `json:"on_event"`
OnTimer *time.Duration `json:"on_timer"`
}
type ChatbotRuleTriggerCommand struct {
Command string `json:"command"`
Restrict *ChatbotRuleTriggerCommandRestriction `json:"restrict"`
Timeout time.Duration `json:"timeout"`
}
type ChatbotRuleTriggerCommandRestriction struct {
Bypass *ChatbotRuleTriggerCommandRestrictionBypass `json:"bypass"`
ToAdmin bool `json:"to_admin"`
ToFollower bool `json:"to_follower"`
ToMod bool `json:"to_mod"`
ToStreamer bool `json:"to_streamer"`
ToSubscriber bool `json:"to_subscriber"`
ToRant int `json:"to_rant"`
}
type ChatbotRuleTriggerCommandRestrictionBypass struct {
IfAdmin bool `json:"if_admin"`
IfMod bool `json:"if_mod"`
IfStreamer bool `json:"if_streamer"`
}
type ChatbotRuleTriggerEvent struct {
OnFollow bool `json:"on_follow"`
OnSubscribe bool `json:"on_subscribe"`
OnRaid bool `json:"on_raid"`
OnRant int `json:"on_rant"`
}
func (a *App) OpenFileDialog() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
a.logError.Println("error getting home directory:", err)
return "", fmt.Errorf("Error opening file explorer. Try again.")
}
filepath, err := runtime.OpenFileDialog(a.wails, runtime.OpenDialogOptions{DefaultDirectory: home})
if err != nil {
a.logError.Println("error opening file dialog:", err)
return "", fmt.Errorf("Error opening file explorer. Try again.")
}
return filepath, err
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,4 +1,5 @@
import chess_rook from './icons/Font-Awesome/chess-rook.png'; import chess_rook from './icons/Font-Awesome/chess-rook.png';
import chevron_left from './icons/twbs/chevron-left.png';
import chevron_right from './icons/twbs/chevron-right.png'; import chevron_right from './icons/twbs/chevron-right.png';
import circle_green_background from './icons/twbs/circle-green-background.png'; import circle_green_background from './icons/twbs/circle-green-background.png';
import circle_red_background from './icons/twbs/circle-red-background.png'; import circle_red_background from './icons/twbs/circle-red-background.png';
@ -10,6 +11,7 @@ import heart from './icons/twbs/heart-fill.png';
import pause from './icons/twbs/pause-circle-green.png'; import pause from './icons/twbs/pause-circle-green.png';
import play from './icons/twbs/play-circle-green.png'; import play from './icons/twbs/play-circle-green.png';
import plus_circle from './icons/twbs/plus-circle-fill.png'; import plus_circle from './icons/twbs/plus-circle-fill.png';
import robot from './icons/Font-Awesome/robot.png';
import star from './icons/twbs/star-fill.png'; import star from './icons/twbs/star-fill.png';
import thumbs_down from './icons/twbs/hand-thumbs-down-fill.png'; import thumbs_down from './icons/twbs/hand-thumbs-down-fill.png';
import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png'; import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png';
@ -17,6 +19,7 @@ import x_lg from './icons/twbs/x-lg.png';
import logo from './logo/logo.png'; import logo from './logo/logo.png';
export const ChessRook = chess_rook; export const ChessRook = chess_rook;
export const ChevronLeft = chevron_left;
export const ChevronRight = chevron_right; export const ChevronRight = chevron_right;
export const CircleGreenBackground = circle_green_background; export const CircleGreenBackground = circle_green_background;
export const CircleRedBackground = circle_red_background; export const CircleRedBackground = circle_red_background;
@ -29,6 +32,7 @@ export const Logo = logo;
export const Pause = pause; export const Pause = pause;
export const Play = play; export const Play = play;
export const PlusCircle = plus_circle; export const PlusCircle = plus_circle;
export const Robot = robot;
export const Star = star; export const Star = star;
export const ThumbsDown = thumbs_down; export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up; export const ThumbsUp = thumbs_up;

View file

@ -0,0 +1,360 @@
.chatbot {
background-color: #344453;
display: flex;
flex-direction: column;
height: 100%;
min-width: 500px;
width: 100%;
}
.chatbot-header {
align-items: center;
border-bottom: 1px solid #061726;
box-sizing: border-box;
display: flex;
flex-direction: row;
min-height: 55px;
justify-content: space-between;
padding: 10px 20px;
width: 100%;
}
.chatbot-header-button {
align-items: center;
background-color: #344453;
border: none;
display: flex;
justify-content: center;
padding-left: 10px;
padding-right: 0px;
}
.chatbot-header-button:hover {
cursor: pointer;
}
.chatbot-header-button-icon {
height: 24px;
width: 24px;
}
.chatbot-header-left {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.chatbot-header-icon {
height: 28px;
width: 28px;
padding-right: 10px;
}
.chatbot-header-icon-back {
height: 28px;
width: 28px;
}
.chatbot-header-icon-back:hover {
/* background-color: #415568; */
cursor: pointer;
}
.chatbot-header-right {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.chatbot-header-title {
color: #eee;
font-family: sans-serif;
font-size: 20px;
font-weight: bold;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chatbot-list {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 0px 10px;
}
.chatbot-list-button {
align-items: center;
background-color: #344453;
border: none;
border-radius: 3px;
display: flex;
justify-content: start;
padding: 15px 10px;
width: 100%;
}
.chatbot-list-button:hover {
background-color: #415568;
cursor: pointer;
}
.chatbot-list-item {
}
.chatbot-list-item-name {
color: #eee;
display: inline-block;
font-family: sans-serif;
font-size: 18px;
font-weight: bold;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* width: 100%; */
}
.chatbot-modal-form {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
.chatbot-modal-input {
background-color: #061726;
border: none;
border-radius: 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.chatbot-modal-label {
color: white;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
width: 100%;
}
.chatbot-modal-label-warning {
color: #f23160;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
width: 100%;
}
.chatbot-modal-description {
color: #eee;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
text-align: center;
width: 100%;
}
.chatbot-modal-description-warning {
color: #f23160;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
text-align: center;
width: 100%;
}
.chatbot-modal-pages {
background-color: white;
/* border: 1px solid #D6E0EA; */
border-radius: 5px;
height: 100%;
overflow: auto;
width: 80%;
}
.chatbot-modal-page {
align-items: center;
display: flex;
}
.chatbot-modal-page-selected {
background-color: #85c742;
}
.chatbot-modal-page-button {
background-color: white;
border: none;
border-radius: 5px;
color: #061726;
font-family: sans-serif;
font-size: 18px;
font-weight: bold;
overflow: hidden;
padding: 10px 10px;
text-align: left;
white-space: nowrap;
width: 100%;
}
.chatbot-modal-page-button:hover {
background-color: #85c742;
cursor: pointer;
}
.chatbot-modal-setting {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 10px;
width: 100%;
}
.chatbot-modal-setting-description {
color: #eee;
font-family: sans-serif;
font-size: 16px;
}
.chatbot-modal-textarea {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.chatbot-modal-toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.chatbot-modal-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.chatbot-modal-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #495a6a;
-webkit-transition: .4s;
transition: .4s;
}
.chatbot-modal-toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
.choose-file {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.choose-file-button-box {
min-width: 100px;
width: 100px;
}
.choose-file-button {
background-color: #85c742;
border: none;
border-radius: 5px;
color: #061726;
cursor: pointer;
font-size: 16px;
text-decoration: none;
/* width: 200px; */
width: 100%;
}
.choose-file-path {
color: #eee;
font-family: monospace;
font-size: 16px;
overflow: scroll;
margin-left: 5px;
white-space: nowrap;
}
input:checked + .chatbot-modal-toggle-slider {
background-color: #85c742;
}
input:checked + .chatbot-modal-toggle-slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.chatbot-modal-toggle-slider.round {
border-radius: 34px;
}
.chatbot-modal-toggle-slider.round:before {
border-radius: 50%;
}
.timer-input {
border: none;
border-radius: 34px;
box-sizing: border-box;
font-family: monospace;
font-size: 24px;
outline: none;
padding: 5px 10px 5px 10px;
text-align: right;
}
.timer-input-zero::placeholder {
text-align: center;
}
.timer-input-value::placeholder {
color: black;
opacity: 1;
text-align: center;
}

View file

@ -0,0 +1,962 @@
import { useEffect, useState } from 'react';
import { Modal, SmallModal } from './Modal';
import {
AccountList,
ChatbotList,
DeleteChatbot,
NewChatbot,
OpenFileDialog,
UpdateChatbot,
} from '../../wailsjs/go/main/App';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { ChevronLeft, ChevronRight, Gear, PlusCircle, Robot } from '../assets';
import './ChatBot.css';
function ChatBot(props) {
const [chatbots, setChatbots] = useState([]);
const [deleteChatbot, setDeleteChatbot] = useState(false);
const [editChatbot, setEditChatbot] = useState(null);
const [error, setError] = useState('');
const [openChatbot, setOpenChatbot] = useState(null);
const [openNewChatbot, setOpenNewChatbot] = useState(false);
const [openNewRule, setOpenNewRule] = useState(false);
const [chatbotSettings, setChatbotSettings] = useState(true);
useEffect(() => {
EventsOn('ChatbotList', (event) => {
setChatbots(event);
if (openChatbot !== null) {
for (const chatbot of event) {
if (chatbot.id === openChatbot.id) {
setOpenChatbot(chatbot);
}
}
}
});
}, []);
useEffect(() => {
ChatbotList()
.then((response) => {
setChatbots(response);
})
.catch((error) => {
setError(error);
});
}, []);
const open = (chatbot) => {
setOpenChatbot(chatbot);
};
const closeEdit = () => {
setEditChatbot(null);
};
const openEdit = (chatbot) => {
setEditChatbot(chatbot);
};
const openNew = () => {
setOpenNewChatbot(true);
};
const sortChatbots = () => {
let sorted = [...chatbots].sort((a, b) =>
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
);
return sorted;
};
const confirmDelete = () => {
DeleteChatbot(openChatbot)
.then(() => {
setDeleteChatbot(false);
setEditChatbot(null);
setOpenChatbot(null);
})
.catch((err) => {
setDeleteChatbot(false);
setError(err);
});
};
return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
{openNewChatbot && (
<ModalChatbot
cancelButton={'Cancel'}
onClose={() => setOpenNewChatbot(false)}
show={setOpenNewChatbot}
submit={NewChatbot}
submitButton={'Create'}
submittingButton={'Creating...'}
title={'New Chatbot'}
/>
)}
{editChatbot !== null && (
<ModalChatbot
chatbot={editChatbot}
onClose={closeEdit}
deleteButton={'Delete'}
onDelete={() => setDeleteChatbot(true)}
show={editChatbot !== null}
submit={UpdateChatbot}
submitButton={'Update'}
submittingButton={'Updating...'}
title={'Edit Chatbot'}
/>
)}
{deleteChatbot && (
<SmallModal
cancelButton={'Cancel'}
onCancel={() => setDeleteChatbot(false)}
onClose={() => setDeleteChatbot(false)}
show={deleteChatbot}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Delete Chatbot'}
message={
'Are you sure you want to delete the chatbot? All rules associated with this chatbot will be deleted as well.'
}
submitButton={'OK'}
onSubmit={confirmDelete}
/>
)}
{openNewRule && (
<ModalNewRule onClose={() => setOpenNewRule(false)} show={openNewRule} />
)}
<div className='chatbot'>
{openChatbot === null ? (
<>
<div className='chatbot-header'>
<div className='chatbot-header-left'>
<img className='chatbot-header-icon' src={Robot} />
<span className='chatbot-header-title'>Bots</span>
</div>
<div className='chatbot-header-right'>
<button className='chatbot-header-button' onClick={openNew}>
<img className='chatbot-header-button-icon' src={PlusCircle} />
</button>
</div>
</div>
<div className='chatbot-list'>
{sortChatbots().map((chatbot, index) => (
<ChatbotListItem chatbot={chatbot} key={index} onClick={open} />
))}
</div>
</>
) : (
<div className='chatbot-header'>
<div className='chatbot-header-left'>
<img
className='chatbot-header-icon-back'
onClick={() => setOpenChatbot(null)}
src={ChevronLeft}
/>
</div>
<span className='chatbot-header-title'>{openChatbot.name}</span>
<div className='chatbot-header-right'>
<button
className='chatbot-header-button'
onClick={() => openEdit(openChatbot)}
>
<img className='chatbot-header-button-icon' src={Gear} />
</button>
<button
className='chatbot-header-button'
onClick={() => setOpenNewRule(true)}
>
<img className='chatbot-header-button-icon' src={PlusCircle} />
</button>
</div>
</div>
)}
</div>
</>
);
}
export default ChatBot;
function ChatbotListItem(props) {
return (
<div className='chatbot-list-item'>
<button className='chatbot-list-button' onClick={() => props.onClick(props.chatbot)}>
<span className='chatbot-list-item-name'>{props.chatbot.name}</span>
</button>
</div>
);
}
function ModalChatbot(props) {
const [error, setError] = useState('');
const [id, setId] = useState(props.chatbot === undefined ? null : props.chatbot.id);
const [loading, setLoading] = useState(false);
const [name, setName] = useState(props.chatbot === undefined ? '' : props.chatbot.name);
const updateName = (event) => {
if (loading) {
return;
}
setName(event.target.value);
};
const [nameValid, setNameValid] = useState(true);
const [url, setUrl] = useState(props.chatbot === undefined ? '' : props.chatbot.url);
const updateUrl = (event) => {
if (loading) {
return;
}
setUrl(event.target.value);
};
useEffect(() => {
if (loading) {
props
.submit({ id: id, name: name, url: url })
.then(() => {
reset();
props.onClose();
})
.catch((err) => {
setLoading(false);
setError(err);
});
}
}, [loading]);
const close = () => {
if (loading) {
return;
}
reset();
props.onClose();
};
const reset = () => {
setLoading(false);
setName('');
setNameValid(true);
};
const submit = () => {
if (name == '') {
setNameValid(false);
return;
}
setLoading(true);
};
return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
<Modal
cancelButton={props.cancelButton}
onCancel={close}
onClose={close}
deleteActive={true}
deleteButton={props.deleteButton}
onDelete={props.onDelete}
show={props.show}
style={{ minWidth: '400px', maxWidth: '400px', maxHeight: '400px' }}
submitButton={loading ? props.submittingButton : props.submitButton}
onSubmit={submit}
title={props.title}
>
<div className='chatbot-modal-form'>
{nameValid ? (
<label className='chatbot-modal-label'>Chatbot Name</label>
) : (
<label className='chatbot-modal-label-warning'>
Chatbot Name - Please enter a valid name
</label>
)}
<input
className='chatbot-modal-input'
onChange={updateName}
placeholder={'Name'}
type={'text'}
value={name}
></input>
<label className='chatbot-modal-label'>Live Stream URL</label>
<input
className='chatbot-modal-input'
onChange={updateUrl}
placeholder={'https://rumble.com'}
type={'text'}
value={url}
></input>
</div>
</Modal>
</>
);
}
function ModalNewRule(props) {
const [back, setBack] = useState([]);
const [rule, setRule] = useState({});
const [stage, setStage] = useState('trigger');
const updateStage = (next, reverse) => {
setBack([...back, { stage: stage, reverse: reverse }]);
setStage(next);
};
const goBack = () => {
if (back.length === 0) {
return;
}
const last = back.at(-1);
setStage(last.stage);
if (last.reverse !== undefined && last.reverse !== null) {
setRule(last.reverse(rule));
}
setBack(back.slice(0, back.length - 1));
};
const submit = () => {};
return (
<>
{stage === 'trigger' && (
<ModalNewRuleTrigger
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'trigger-timer' && (
<ModalNewRuleTriggerTimer
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'message' && (
<ModalNewRuleMessage
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'sender' && (
<ModalNewRuleSender
onBack={goBack}
onClose={props.onClose}
rule={rule}
setRule={setRule}
setStage={updateStage}
show={props.show}
/>
)}
{stage === 'review' && (
<ModalNewRuleReview
onBack={goBack}
onClose={props.onClose}
rule={rule}
show={props.show}
onSubmit={submit}
/>
)}
</>
);
}
function ModalNewRuleTrigger(props) {
const next = (stage) => {
const rule = props.rule;
rule.trigger = {};
props.setRule(rule);
props.setStage(stage, reverse);
};
const reverse = (rule) => {
rule.trigger = null;
return rule;
};
return (
<Modal
onClose={props.onClose}
show={props.show}
style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }}
>
<div className='modal-add-account-channel'>
<span className='modal-add-account-channel-title'>Choose Rule Trigger</span>
<div className='modal-add-account-channel-body'>
<button
className='modal-add-account-channel-button'
onClick={() => next('trigger-command')}
>
<div className='modal-add-account-channel-button-left'>
<span>Command</span>
</div>
<img
className='modal-add-account-channel-button-right-icon'
src={ChevronRight}
/>
</button>
<button
className='modal-add-account-channel-button'
onClick={() => next('trigger-stream_event')}
>
<div className='modal-add-account-channel-button-left'>
<span>Stream Event</span>
</div>
<img
className='modal-add-account-channel-button-right-icon'
src={ChevronRight}
/>
</button>
<button
className='modal-add-account-channel-button'
onClick={() => next('trigger-timer')}
>
<div className='modal-add-account-channel-button-left'>
<span>Timer</span>
</div>
<img
className='modal-add-account-channel-button-right-icon'
src={ChevronRight}
/>
</button>
</div>
<div></div>
</div>
</Modal>
);
}
function ModalNewRuleTriggerTimer(props) {
const [validTimer, setValidTimer] = useState(true);
const [timer, setTimer] = useState(
props.rule.trigger.on_timer !== undefined && props.rule.trigger.on_timer !== null
? props.rule.trigger.on_timer
: ''
);
const back = () => {
const rule = props.rule;
rule.trigger.on_timer = '';
props.setRule(rule);
props.onBack();
};
const next = () => {
if (timer === '') {
setValidTimer(false);
return;
}
const rule = props.rule;
rule.trigger.on_timer = timer;
props.setRule(rule);
props.setStage('message', null);
};
return (
<Modal
cancelButton={'Back'}
onCancel={back}
onClose={props.onClose}
show={props.show}
submitButton={'Next'}
onSubmit={next}
style={{ height: '300px', minHeight: '300px', width: '360px', minWidth: '360px' }}
>
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Set Timer</span>
<span className='modal-add-account-channel-subtitle'>
Chat rule will trigger at the set interval.
</span>
</div>
<div className='modal-add-account-channel-body'>
{validTimer ? (
<span className='chatbot-modal-description'>Enter timer</span>
) : (
<span className='chatbot-modal-description-warning'>
Enter a valid timer interval.
</span>
)}
<Timer timer={timer} setTimer={setTimer} />
</div>
<div style={{ height: '56px' }}></div>
</div>
</Modal>
);
}
function ModalNewRuleMessage(props) {
const [error, setError] = useState('');
const [message, setMessage] = useState(
props.rule.message !== undefined && props.rule.message !== null ? props.rule.message : {}
);
const [refresh, setRefresh] = useState(false);
const [validFile, setValidFile] = useState(true);
const [validText, setValidText] = useState(true);
const back = () => {
const rule = props.rule;
rule.message = null;
props.setRule(rule);
props.onBack();
};
const next = () => {
if (fromFile()) {
if (message.from_file.filepath === undefined || message.from_file.filepath === '') {
setValidFile(false);
return;
}
} else {
if (message.from_text === undefined || message.from_text === '') {
setValidText(false);
return;
}
}
const rule = props.rule;
rule.message = message;
props.setRule(rule);
props.setStage('sender', null);
};
const chooseFile = () => {
OpenFileDialog()
.then((filepath) => {
if (filepath !== '') {
message.from_file.filepath = filepath;
setMessage(message);
setRefresh(!refresh);
}
})
.catch((error) => setError(error));
};
const fromFile = () => {
return message.from_file !== undefined && message.from_file !== null;
};
const toggleFromFile = () => {
if (fromFile()) {
message.from_file = null;
} else {
message.from_file = {};
}
setMessage(message);
setRefresh(!refresh);
};
const randomRead = () => {
if (!fromFile()) {
return false;
}
if (message.from_file.random_read === undefined || message.from_file.random_read === null) {
return false;
}
return message.from_file.random_read;
};
const toggleRandomRead = () => {
if (!fromFile()) {
return;
}
message.from_file.random_read = !randomRead();
setMessage(message);
setRefresh(!refresh);
};
const updateMessageText = (event) => {
message.from_text = event.target.value;
setMessage(message);
};
const updateMessageFilepath = (filepath) => {
if (!fromFile()) {
message.from_file = {};
}
message.from_file = filepath;
setMessage(message);
};
return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
<Modal
cancelButton={'Back'}
onCancel={back}
onClose={props.onClose}
show={props.show}
submitButton={'Next'}
onSubmit={next}
style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }}
>
<div className='modal-add-account-channel'>
<span className='modal-add-account-channel-title'>Add Message</span>
<div className='modal-add-account-channel-body'>
{fromFile() ? (
validFile ? (
<label className='modal-add-channel-label'>Message</label>
) : (
<label className='modal-add-channel-label-warning'>
Select a file
</label>
)
) : validText ? (
<label className='modal-add-channel-label'>Message</label>
) : (
<label className='modal-add-channel-label-warning'>Add a message</label>
)}
{fromFile() ? (
<div className='choose-file'>
<div className='choose-file-button-box'>
<button className='choose-file-button' onClick={chooseFile}>
Choose file
</button>
</div>
<span className='choose-file-path'>
{message.from_file.filepath}
</span>
</div>
) : (
<textarea
className='chatbot-modal-textarea'
onChange={updateMessageText}
rows='4'
value={message.from_text}
/>
)}
<div className='chatbot-modal-setting'>
<label className='chatbot-modal-setting-description'>
Read from file
</label>
<label className='chatbot-modal-toggle-switch'>
<input
checked={fromFile()}
onChange={toggleFromFile}
type='checkbox'
/>
<span className='chatbot-modal-toggle-slider round'></span>
</label>
</div>
{fromFile() && (
<div className='chatbot-modal-setting'>
<label className='chatbot-modal-setting-description'>
Choose lines in random order
</label>
<label className='chatbot-modal-toggle-switch'>
<input
checked={randomRead()}
onChange={toggleRandomRead}
type='checkbox'
/>
<span className='chatbot-modal-toggle-slider round'></span>
</label>
</div>
)}
</div>
<div style={{ height: '29px' }}></div>
</div>
</Modal>
</>
);
}
function ModalNewRuleSender(props) {
const [accounts, setAccounts] = useState({});
const [error, setError] = useState('');
const [sender, setSender] = useState(
props.rule.send_as !== undefined && props.rule.send_as !== null ? props.rule.send_as : {}
);
const [validSender, setValidSender] = useState(true);
useEffect(() => {
AccountList()
.then((response) => {
setAccounts(response);
})
.catch((error) => {
setError(error);
});
}, []);
const back = () => {
const rule = props.rule;
rule.send_as = null;
props.setRule(rule);
props.onBack();
};
const next = () => {
if (sender.username === undefined || sender.username === '') {
setValidSender(false);
return;
}
const rule = props.rule;
rule.send_as = sender;
props.setRule(rule);
props.setStage('review', null);
};
const selectSender = (sender) => {
setSender(sender);
};
const sortChannels = (channels) => {
let sorted = [...channels].sort((a, b) =>
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
);
return sorted;
};
const sortAccounts = () => {
let keys = Object.keys(accounts);
let sorted = [...keys].sort((a, b) =>
accounts[a].account.username.toLowerCase() > accounts[b].account.username.toLowerCase()
? 1
: -1
);
return sorted;
};
const sortPages = () => {
let pages = [];
const keys = sortAccounts();
keys.forEach((key, i) => {
const account = accounts[key];
pages.push({
display: account.account.username,
channel_id: null,
username: account.account.username,
});
const channels = sortChannels(account.channels);
channels.forEach((channel, j) => {
pages.push({
display: channel.name,
channel_id: channel.cid,
username: account.account.username,
});
});
});
return pages;
};
return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
<Modal
cancelButton={'Back'}
onCancel={back}
onClose={props.onClose}
show={props.show}
submitButton={'Next'}
onSubmit={next}
style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }}
>
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Choose sender</span>
</div>
<div className='modal-add-account-channel-body' style={{ height: '60%' }}>
{validSender ? (
<span className='chatbot-modal-description'>Chat As</span>
) : (
<span className='chatbot-modal-description-warning'>
Select an account or channel
</span>
)}
<div className='chatbot-modal-pages'>
{sortPages().map((page, index) => (
<div className={'chatbot-modal-page'} key={index}>
<button
className='chatbot-modal-page-button'
onClick={() => selectSender(page)}
style={{
backgroundColor:
page.display === sender.display ? '#85c742' : '',
}}
>
{page.display}
</button>
</div>
))}
</div>
</div>
<div style={{ height: '29px' }}></div>
</div>
</Modal>
</>
);
}
function ModalNewRuleReview(props) {
const [error, setError] = useState('');
const back = () => {
props.onBack();
};
const submit = () => {
props.onSubmit();
};
return (
<Modal
cancelButton={'Back'}
onCancel={back}
onClose={props.onClose}
show={props.show}
submitButton={'Submit'}
onSubmit={submit}
style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }}
>
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Review</span>
</div>
<div className='modal-add-account-channel-body'>
<div className='chatbot-modal-setting'>
<label className='chatbot-modal-setting-description'>Trigger</label>
<label className='chatbot-modal-setting-description'>Timer</label>
</div>
<div className='chatbot-modal-setting'>
<label className='chatbot-modal-setting-description'>Chat As</label>
<label className='chatbot-modal-setting-description'>tylertravisty</label>
</div>
</div>
<div style={{ height: '29px' }}></div>
</div>
</Modal>
);
}
function Timer(props) {
const updateTimerBackspace = (e) => {
if (props.timer.length === 0) {
return;
}
if (e.keyCode === 8) {
props.setTimer(props.timer.substring(0, props.timer.length - 1));
}
};
const updateTimer = (e) => {
let nums = '0123456789';
let digit = e.target.value;
if (!nums.includes(digit)) {
return;
}
let interval = timerValToInterval(props.timer + digit);
if (interval >= 360000) {
return;
}
if (props.timer.length === 0 && digit === '0') {
return;
}
props.setTimer(props.timer + digit);
};
const timerValToInterval = (val) => {
let prefix = '0'.repeat(6 - val.length);
let t = prefix + val;
let hours = parseInt(t.substring(0, 2));
let minutes = parseInt(t.substring(2, 4));
let seconds = parseInt(t.substring(4, 6));
return hours * 3600 + minutes * 60 + seconds;
};
const printTimer = () => {
if (props.timer === '') {
return '00:00:00';
}
let prefix = '0'.repeat(6 - props.timer.length);
let t = prefix + props.timer;
return t.substring(0, 2) + ':' + t.substring(2, 4) + ':' + t.substring(4, 6);
};
return (
<input
className={
props.timer === ''
? 'timer-input timer-input-zero'
: 'timer-input timer-input-value'
}
onKeyDown={updateTimerBackspace}
onInput={updateTimer}
placeholder={printTimer()}
size='8'
type='text'
value={''}
/>
);
}

View file

@ -5,7 +5,7 @@ export function Modal(props) {
return ( return (
<div <div
className='modal-background' className='modal-background'
onClick={props.onClose} onClick={props.backgroundClose && props.onClose}
style={{ zIndex: props.show ? 8 : -8 }} style={{ zIndex: props.show ? 8 : -8 }}
> >
<div <div

View file

@ -192,7 +192,7 @@
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
/* min-height: 80px; */ min-height: 55px;
justify-content: space-between; justify-content: space-between;
padding: 10px 20px; padding: 10px 20px;
width: 100%; width: 100%;

View file

@ -295,6 +295,7 @@ function PageDetails(props) {
<> <>
{openLogin && ( {openLogin && (
<Modal <Modal
backgroundClose={true}
cancelButton={'Cancel'} cancelButton={'Cancel'}
onCancel={closeLogin} onCancel={closeLogin}
onClose={closeLogin} onClose={closeLogin}
@ -345,6 +346,7 @@ function PageDetails(props) {
)} )}
{openDelete && ( {openDelete && (
<Modal <Modal
backgroundClose={true}
cancelButton={'Cancel'} cancelButton={'Cancel'}
onCancel={resetDelete} onCancel={resetDelete}
onClose={resetDelete} onClose={resetDelete}
@ -384,6 +386,7 @@ function PageDetails(props) {
)} )}
{openApi && ( {openApi && (
<Modal <Modal
backgroundClose={true}
cancelButton={'Cancel'} cancelButton={'Cancel'}
onCancel={closeEditApi} onCancel={closeEditApi}
onClose={closeEditApi} onClose={closeEditApi}

View file

@ -24,7 +24,7 @@ function PageSideBar(props) {
const [accounts, setAccounts] = useState({}); const [accounts, setAccounts] = useState({});
const [error, setError] = useState(''); const [error, setError] = useState('');
const [addOpen, setAddOpen] = useState(false); const [addOpen, setAddOpen] = useState(false);
const [refresh, setRefresh] = useState(false); // const [refresh, setRefresh] = useState(false);
const [scrollY, setScrollY] = useState(0); const [scrollY, setScrollY] = useState(0);
useEffect(() => { useEffect(() => {
@ -41,7 +41,7 @@ function PageSideBar(props) {
.catch((error) => { .catch((error) => {
setError(error); setError(error);
}); });
}, [refresh]); }, []);
const sortAccounts = () => { const sortAccounts = () => {
let keys = Object.keys(accounts); let keys = Object.keys(accounts);
@ -354,7 +354,7 @@ function ModalAdd(props) {
.then(() => { .then(() => {
reset(); reset();
props.onClose(); props.onClose();
props.onRefresh(); //props.onRefresh();
}) })
.catch((error) => { .catch((error) => {
setAddAccountLoading(false); setAddAccountLoading(false);
@ -369,7 +369,7 @@ function ModalAdd(props) {
.then(() => { .then(() => {
reset(); reset();
props.onClose(); props.onClose();
props.onRefresh(); //props.onRefresh();
}) })
.catch((error) => { .catch((error) => {
setAddChannelLoading(false); setAddChannelLoading(false);

View file

@ -4,13 +4,14 @@ import { CircleGreenBackground, Heart } from '../assets';
import PageDetails from '../components/PageDetails'; import PageDetails from '../components/PageDetails';
import PageSideBar from '../components/PageSideBar'; import PageSideBar from '../components/PageSideBar';
import './Dashboard.css'; import './Dashboard.css';
import ChatBot from '../components/ChatBot';
function Dashboard() { function Dashboard() {
return ( return (
<div className='dashboard'> <div className='dashboard'>
<PageSideBar /> <PageSideBar />
<PageDetails /> <PageDetails />
<div style={{ backgroundColor: '#344453', width: '100%', height: '100%' }}></div> <ChatBot />
</div> </div>
); );
} }

View file

@ -6,7 +6,7 @@ toolchain go1.22.0
require ( require (
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/tylertravisty/rumble-livestream-lib-go v0.3.5 github.com/tylertravisty/rumble-livestream-lib-go v0.5.1
github.com/wailsapp/wails/v2 v2.8.0 github.com/wailsapp/wails/v2 v2.8.0
) )
@ -36,10 +36,10 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.10 // indirect github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.22.0 // indirect golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect

View file

@ -60,8 +60,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g= github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk= github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk=
github.com/tylertravisty/rumble-livestream-lib-go v0.3.5 h1:mAf4oYuQ55pXTPsIMVztOlYM8oGsBgsNMJvel2VLgsQ= github.com/tylertravisty/rumble-livestream-lib-go v0.5.1 h1:vq65n/8MOvvg6tHiaHFFfYf25w7yuR1viSoBCjY2DSg=
github.com/tylertravisty/rumble-livestream-lib-go v0.3.5/go.mod h1:rUET5uInouMfB4ekqdGiYeoN5ibOdzU9cCgRE0i57Wg= github.com/tylertravisty/rumble-livestream-lib-go v0.5.1/go.mod h1:Odkqvsn+2eoWV3ePcj257Ga0bdOqV4JBTfOJcQ+Sqf8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@ -74,14 +74,14 @@ github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4X
github.com/wailsapp/wails/v2 v2.8.0 h1:b2NNn99uGPiN6P5bDsnPwOJZWtAOUhNLv7Vl+YxMTr4= github.com/wailsapp/wails/v2 v2.8.0 h1:b2NNn99uGPiN6P5bDsnPwOJZWtAOUhNLv7Vl+YxMTr4=
github.com/wailsapp/wails/v2 v2.8.0/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0= github.com/wailsapp/wails/v2 v2.8.0/go.mod h1:EFUGWkUX3KofO4fmKR/GmsLy3HhPH7NbyOEaMt8lBF0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -93,8 +93,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View file

@ -74,6 +74,7 @@ func (ap *ApiProducer) Shutdown() error {
timer := time.NewTimer(3 * time.Second) timer := time.NewTimer(3 * time.Second)
select { select {
case <-ap.closeCh: case <-ap.closeCh:
timer.Stop()
close(ap.Ch) close(ap.Ch)
case <-timer.C: case <-timer.C:
return pkgErr("", fmt.Errorf("not all producers were stopped")) return pkgErr("", fmt.Errorf("not all producers were stopped"))
@ -128,7 +129,7 @@ func (ap *ApiProducer) Stop(name string) error {
} }
func (ap *ApiProducer) run(ctx context.Context, producer *apiProducer) { func (ap *ApiProducer) run(ctx context.Context, producer *apiProducer) {
client := &rumblelivestreamlib.Client{StreamKey: producer.url} client := &rumblelivestreamlib.Client{ApiKey: producer.url}
for { for {
resp, err := apiQuery(client) resp, err := apiQuery(client)
if err != nil { if err != nil {

205
v1/internal/events/chat.go Normal file
View file

@ -0,0 +1,205 @@
package events
import (
"context"
"fmt"
"log"
"sync"
"time"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
)
type Chat struct {
Message rumblelivestreamlib.ChatView
Stop bool
Url string
}
type chatProducer struct {
cancel context.CancelFunc
cancelMu sync.Mutex
client *rumblelivestreamlib.Client
url string
}
type chatProducerValFunc func(*chatProducer) error
func runChatProducerValFuncs(c *chatProducer, fns ...chatProducerValFunc) error {
if c == nil {
return fmt.Errorf("chat producer is nil")
}
for _, fn := range fns {
err := fn(c)
if err != nil {
return err
}
}
return nil
}
func chatProducerRequireClient(c *chatProducer) error {
if c.client == nil {
return fmt.Errorf("client is nil")
}
return nil
}
type ChatProducer struct {
Ch chan Chat
close bool
closeMu sync.Mutex
closeCh chan bool
logError *log.Logger
logInfo *log.Logger
producers map[string]*chatProducer
producersMu sync.Mutex
}
func NewChatProducer(logError *log.Logger, logInfo *log.Logger) *ChatProducer {
return &ChatProducer{
Ch: make(chan Chat, 10),
closeCh: make(chan bool),
logError: logError,
logInfo: logInfo,
producers: map[string]*chatProducer{},
}
}
// func (cp *ChatProducer) Active(url string) bool {
// cp.producersMu.Lock()
// defer cp.producersMu.Unlock()
// _, active := cp.producers[url]
// return active
// }
func (cp *ChatProducer) Start(liveStreamUrl string) (string, error) {
if liveStreamUrl == "" {
return "", pkgErr("", fmt.Errorf("url is empty"))
}
client, err := rumblelivestreamlib.NewClient(rumblelivestreamlib.NewClientOptions{LiveStreamUrl: liveStreamUrl})
if err != nil {
return "", pkgErr("error creating new rumble client", err)
}
chatInfo, err := client.ChatInfo(false)
if err != nil {
return "", pkgErr("error getting chat info", err)
}
chatStreamUrl := chatInfo.StreamUrl()
cp.producersMu.Lock()
defer cp.producersMu.Unlock()
if _, active := cp.producers[chatStreamUrl]; active {
return chatStreamUrl, nil
}
ctx, cancel := context.WithCancel(context.Background())
producer := &chatProducer{
cancel: cancel,
client: client,
url: chatStreamUrl,
}
cp.producers[chatStreamUrl] = producer
go cp.run(ctx, producer)
return chatStreamUrl, nil
}
func (cp *ChatProducer) Stop(chatStreamUrl string) error {
cp.producersMu.Lock()
producer, exists := cp.producers[chatStreamUrl]
if !exists {
return pkgErr("", fmt.Errorf("producer does not exist for chat stream: %s", chatStreamUrl))
}
cp.producersMu.Unlock()
producer.cancelMu.Lock()
if producer.cancel != nil {
producer.cancel()
}
producer.cancelMu.Unlock()
return nil
}
func (cp *ChatProducer) run(ctx context.Context, producer *chatProducer) {
err := runChatProducerValFuncs(
producer,
chatProducerRequireClient,
)
if err != nil {
cp.logError.Println(pkgErr("invalid chat producer", err))
return
}
for {
err = producer.client.StartChatStream(cp.handleChat(producer), cp.handleError(producer))
if err != nil {
cp.logError.Println(pkgErr("error starting chat stream", err))
cp.stop(producer)
return
}
timer := time.NewTimer(90 * time.Minute)
select {
case <-ctx.Done():
timer.Stop()
producer.client.StopChatStream()
cp.stop(producer)
return
case <-timer.C:
}
}
}
func (cp *ChatProducer) handleChat(p *chatProducer) func(cv rumblelivestreamlib.ChatView) {
return func(cv rumblelivestreamlib.ChatView) {
if p == nil {
return
}
cp.Ch <- Chat{Message: cv, Url: p.url}
}
}
func (cp *ChatProducer) handleError(p *chatProducer) func(err error) {
return func(err error) {
cp.logError.Println(pkgErr("chat stream returned error", err))
p.cancelMu.Lock()
if p.cancel != nil {
p.cancel()
}
p.cancelMu.Unlock()
}
}
func (cp *ChatProducer) stop(p *chatProducer) {
if p == nil {
return
}
cp.Ch <- Chat{Stop: true, Url: p.url}
cp.producersMu.Lock()
delete(cp.producers, p.url)
remaining := len(cp.producers)
cp.producersMu.Unlock()
cp.closeMu.Lock()
if remaining == 0 && cp.close {
select {
case cp.closeCh <- true:
default:
break
}
}
cp.closeMu.Unlock()
return
}

View file

@ -9,6 +9,7 @@ type Producers struct {
logError *log.Logger logError *log.Logger
logInfo *log.Logger logInfo *log.Logger
ApiP *ApiProducer ApiP *ApiProducer
ChatP *ChatProducer
} }
func (p *Producers) Startup() error { func (p *Producers) Startup() error {
@ -61,3 +62,11 @@ func WithApiProducer() ProducersInit {
return nil return nil
} }
} }
func WithChatProducer() ProducersInit {
return func(p *Producers) error {
p.ChatP = NewChatProducer(p.logError, p.logInfo)
return nil
}
}

View file

@ -325,7 +325,7 @@ type accountValFunc func(*Account) error
func runAccountValFuncs(a *Account, fns ...accountValFunc) error { func runAccountValFuncs(a *Account, fns ...accountValFunc) error {
if a == nil { if a == nil {
return fmt.Errorf("account cannot be nil") return fmt.Errorf("account is nil")
} }
for _, fn := range fns { for _, fn := range fns {

View file

@ -337,7 +337,7 @@ type channelValFunc func(*Channel) error
func runChannelValFuncs(c *Channel, fns ...channelValFunc) error { func runChannelValFuncs(c *Channel, fns ...channelValFunc) error {
if c == nil { if c == nil {
return fmt.Errorf("channel cannot be nil") return fmt.Errorf("channel is nil")
} }
for _, fn := range fns { for _, fn := range fns {

View file

@ -0,0 +1,313 @@
package models
import (
"database/sql"
"fmt"
)
const (
chatbotColumns = "id, name, url"
chatbotTable = "chatbot"
)
type Chatbot struct {
ID *int64 `json:"id"`
Name *string `json:"name"`
Url *string `json:"url"`
}
func (c *Chatbot) values() []any {
return []any{c.ID, c.Name, c.Url}
}
func (c *Chatbot) valuesNoID() []any {
return c.values()[1:]
}
func (c *Chatbot) valuesEndID() []any {
vals := c.values()
return append(vals[1:], vals[0])
}
type sqlChatbot struct {
id sql.NullInt64
name sql.NullString
url sql.NullString
}
func (sc *sqlChatbot) scan(r Row) error {
return r.Scan(&sc.id, &sc.name, &sc.url)
}
func (sc sqlChatbot) toChatbot() *Chatbot {
var c Chatbot
c.ID = toInt64(sc.id)
c.Name = toString(sc.name)
c.Url = toString(sc.url)
return &c
}
type ChatbotService interface {
All() ([]Chatbot, error)
AutoMigrate() error
ByID(id int64) (*Chatbot, error)
ByName(name string) (*Chatbot, error)
Create(c *Chatbot) (int64, error)
Delete(c *Chatbot) error
DestructiveReset() error
Update(c *Chatbot) error
}
func NewChatbotService(db *sql.DB) ChatbotService {
return &chatbotService{
Database: db,
}
}
var _ ChatbotService = &chatbotService{}
type chatbotService struct {
Database *sql.DB
}
func (cs *chatbotService) All() ([]Chatbot, error) {
selectQ := fmt.Sprintf(`
SELECT %s
FROM "%s"
`, chatbotColumns, chatbotTable)
rows, err := cs.Database.Query(selectQ)
if err != nil {
return nil, pkgErr("error executing select query", err)
}
defer rows.Close()
chatbots := []Chatbot{}
for rows.Next() {
sc := &sqlChatbot{}
err = sc.scan(rows)
if err != nil {
return nil, pkgErr("error scanning row", err)
}
chatbots = append(chatbots, *sc.toChatbot())
}
err = rows.Err()
if err != nil && err != sql.ErrNoRows {
return nil, pkgErr("error iterating over rows", err)
}
return chatbots, nil
}
func (cs *chatbotService) AutoMigrate() error {
err := cs.createChatbotTable()
if err != nil {
return pkgErr(fmt.Sprintf("error creating %s table", chatbotTable), err)
}
return nil
}
func (cs *chatbotService) createChatbotTable() error {
createQ := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS "%s" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
url TEXT
)
`, chatbotTable)
_, err := cs.Database.Exec(createQ)
if err != nil {
return fmt.Errorf("error executing create query: %v", err)
}
return nil
}
func (cs *chatbotService) ByID(id int64) (*Chatbot, error) {
err := runChatbotValFuncs(
&Chatbot{ID: &id},
chatbotRequireID,
)
if err != nil {
return nil, pkgErr("", err)
}
selectQ := fmt.Sprintf(`
SELECT %s
FROM "%s"
WHERE id=?
`, chatbotColumns, chatbotTable)
var sc sqlChatbot
row := cs.Database.QueryRow(selectQ, id)
err = sc.scan(row)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, pkgErr("error executing select query", err)
}
return sc.toChatbot(), nil
}
func (cs *chatbotService) ByName(name string) (*Chatbot, error) {
err := runChatbotValFuncs(
&Chatbot{Name: &name},
chatbotRequireName,
)
if err != nil {
return nil, pkgErr("", err)
}
selectQ := fmt.Sprintf(`
SELECT %s
FROM "%s"
WHERE name=?
`, chatbotColumns, chatbotTable)
var sc sqlChatbot
row := cs.Database.QueryRow(selectQ, name)
err = sc.scan(row)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, pkgErr("error executing select query", err)
}
return sc.toChatbot(), nil
}
func (cs *chatbotService) Create(c *Chatbot) (int64, error) {
err := runChatbotValFuncs(
c,
chatbotRequireName,
)
if err != nil {
return -1, pkgErr("invalid chatbot", err)
}
columns := columnsNoID(chatbotColumns)
insertQ := fmt.Sprintf(`
INSERT INTO "%s" (%s)
VALUES (%s)
RETURNING id
`, chatbotTable, columns, values(columns))
var id int64
row := cs.Database.QueryRow(insertQ, c.valuesNoID()...)
err = row.Scan(&id)
if err != nil {
return -1, pkgErr("error executing insert query", err)
}
return id, nil
}
func (cs *chatbotService) Delete(c *Chatbot) error {
err := runChatbotValFuncs(
c,
chatbotRequireID,
)
if err != nil {
return pkgErr("invalid chatbot", err)
}
deleteQ := fmt.Sprintf(`
DELETE FROM "%s"
WHERE id=?
`, chatbotTable)
_, err = cs.Database.Exec(deleteQ, c.ID)
if err != nil {
return pkgErr("error executing delete query", err)
}
return nil
}
func (cs *chatbotService) DestructiveReset() error {
err := cs.dropChatbotTable()
if err != nil {
return pkgErr(fmt.Sprintf("error dropping %s table", chatbotTable), err)
}
return nil
}
func (cs *chatbotService) dropChatbotTable() error {
dropQ := fmt.Sprintf(`
DROP TABLE IF EXISTS "%s"
`, chatbotTable)
_, err := cs.Database.Exec(dropQ)
if err != nil {
return fmt.Errorf("error executing drop query: %v", err)
}
return nil
}
func (cs *chatbotService) Update(c *Chatbot) error {
err := runChatbotValFuncs(
c,
chatbotRequireID,
chatbotRequireName,
)
if err != nil {
return pkgErr("invalid chatbot", err)
}
columns := columnsNoID(chatbotColumns)
updateQ := fmt.Sprintf(`
UPDATE "%s"
SET %s
WHERE id=?
`, chatbotTable, set(columns))
_, err = cs.Database.Exec(updateQ, c.valuesEndID()...)
if err != nil {
return pkgErr("error executing update query", err)
}
return nil
}
type chatbotValFunc func(*Chatbot) error
func runChatbotValFuncs(c *Chatbot, fns ...chatbotValFunc) error {
if c == nil {
return fmt.Errorf("chatbot is nil")
}
for _, fn := range fns {
err := fn(c)
if err != nil {
return err
}
}
return nil
}
func chatbotRequireID(c *Chatbot) error {
if c.ID == nil || *c.ID < 1 {
return ErrChatbotInvalidID
}
return nil
}
func chatbotRequireName(c *Chatbot) error {
if c.Name == nil || *c.Name == "" {
return ErrChatbotInvalidName
}
return nil
}

View file

@ -0,0 +1,224 @@
package models
import (
"database/sql"
"fmt"
)
const (
chatbotRuleColumns = "id, chatbot_id, name, rule"
chatbotRuleTable = "chatbot_rule"
)
type ChatbotRule struct {
ID *int64 `json:"id"`
ChatbotID *int64 `json:"chatbot_id"`
Rule *string `json:"rule"`
}
func (c *ChatbotRule) values() []any {
return []any{c.ID, c.ChatbotID, c.Rule}
}
func (c *ChatbotRule) valuesNoID() []any {
return c.values()[1:]
}
func (c *ChatbotRule) valuesEndID() []any {
vals := c.values()
return append(vals[1:], vals[0])
}
type sqlChatbotRule struct {
id sql.NullInt64
chatbotID sql.NullInt64
rule sql.NullString
}
func (sc *sqlChatbotRule) scan(r Row) error {
return r.Scan(&sc.id, &sc.chatbotID, &sc.rule)
}
func (sc sqlChatbotRule) toChatbotRule() *ChatbotRule {
var c ChatbotRule
c.ID = toInt64(sc.id)
c.ChatbotID = toInt64(sc.chatbotID)
c.Rule = toString(sc.rule)
return &c
}
type ChatbotRuleService interface {
AutoMigrate() error
Create(c *ChatbotRule) (int64, error)
Delete(c *ChatbotRule) error
DestructiveReset() error
Update(c *ChatbotRule) error
}
func NewChatbotRuleService(db *sql.DB) ChatbotRuleService {
return &chatbotRuleService{
Database: db,
}
}
var _ ChatbotRuleService = &chatbotRuleService{}
type chatbotRuleService struct {
Database *sql.DB
}
func (cs *chatbotRuleService) AutoMigrate() error {
err := cs.createChatbotRuleTable()
if err != nil {
return pkgErr(fmt.Sprintf("error creating %s table", chatbotRuleTable), err)
}
return nil
}
func (cs *chatbotRuleService) createChatbotRuleTable() error {
createQ := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS "%s" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
chatbot_id INTEGER NOT NULL,
rule TEXT NOT NULL
FOREIGN KEY (chatbot_id) REFERENCES "%s" (id)
)
`, chatbotRuleTable, chatbotTable)
_, err := cs.Database.Exec(createQ)
if err != nil {
return fmt.Errorf("error executing create query: %v", err)
}
return nil
}
func (cs *chatbotRuleService) Create(c *ChatbotRule) (int64, error) {
err := runChatbotRuleValFuncs(
c,
chatbotRuleRequireRule,
)
if err != nil {
return -1, pkgErr("invalid chat rule", err)
}
columns := columnsNoID(chatbotRuleColumns)
insertQ := fmt.Sprintf(`
INSERT INTO "%s" (%s)
VALUES (%s)
RETURNING id
`, chatbotRuleTable, columns, values(columns))
var id int64
row := cs.Database.QueryRow(insertQ, c.valuesNoID()...)
err = row.Scan(&id)
if err != nil {
return -1, pkgErr("error executing insert query", err)
}
return id, nil
}
func (cs *chatbotRuleService) Delete(c *ChatbotRule) error {
err := runChatbotRuleValFuncs(
c,
chatbotRuleRequireID,
)
if err != nil {
return pkgErr("invalid chat rule", err)
}
deleteQ := fmt.Sprintf(`
DELETE FROM "%s"
WHERE id=?
`, chatbotRuleTable)
_, err = cs.Database.Exec(deleteQ, c.ID)
if err != nil {
return pkgErr("error executing delete query", err)
}
return nil
}
func (cs *chatbotRuleService) DestructiveReset() error {
err := cs.dropChatbotRuleTable()
if err != nil {
return pkgErr(fmt.Sprintf("error dropping %s table", chatbotRuleTable), err)
}
return nil
}
func (cs *chatbotRuleService) dropChatbotRuleTable() error {
dropQ := fmt.Sprintf(`
DROP TABLE IF EXISTS "%s"
`, chatbotRuleTable)
_, err := cs.Database.Exec(dropQ)
if err != nil {
return fmt.Errorf("error executing drop query: %v", err)
}
return nil
}
func (cs *chatbotRuleService) Update(c *ChatbotRule) error {
err := runChatbotRuleValFuncs(
c,
chatbotRuleRequireID,
chatbotRuleRequireRule,
)
if err != nil {
return pkgErr("invalid chat rule", err)
}
columns := columnsNoID(chatbotRuleColumns)
updateQ := fmt.Sprintf(`
UPDATE "%s"
SET %s
WHERE id=?
`, chatbotRuleTable, set(columns))
_, err = cs.Database.Exec(updateQ, c.valuesEndID()...)
if err != nil {
return pkgErr("error executing update query", err)
}
return nil
}
type chatbotRuleValFunc func(*ChatbotRule) error
func runChatbotRuleValFuncs(c *ChatbotRule, fns ...chatbotRuleValFunc) error {
if c == nil {
return fmt.Errorf("chat rule is nil")
}
for _, fn := range fns {
err := fn(c)
if err != nil {
return err
}
}
return nil
}
func chatbotRuleRequireID(c *ChatbotRule) error {
if c.ID == nil || *c.ID < 1 {
return ErrChatbotRuleInvalidID
}
return nil
}
func chatbotRuleRequireRule(c *ChatbotRule) error {
if c.Rule == nil || *c.Rule == "" {
return ErrChatbotRuleInvalidRule
}
return nil
}

View file

@ -13,6 +13,12 @@ const (
ErrChannelInvalidCID ValidatorError = "invalid channel CID" ErrChannelInvalidCID ValidatorError = "invalid channel CID"
ErrChannelInvalidID ValidatorError = "invalid channel ID" ErrChannelInvalidID ValidatorError = "invalid channel ID"
ErrChannelInvalidName ValidatorError = "invalid channel name" ErrChannelInvalidName ValidatorError = "invalid channel name"
ErrChatbotInvalidID ValidatorError = "invalid chatbot id"
ErrChatbotInvalidName ValidatorError = "invalid chatbot name"
ErrChatbotRuleInvalidID ValidatorError = "invalid chatbot rule id"
ErrChatbotRuleInvalidRule ValidatorError = "invalid chatbot rule rule"
) )
func pkgErr(prefix string, err error) error { func pkgErr(prefix string, err error) error {

View file

@ -17,6 +17,7 @@ type Services struct {
AccountS AccountService AccountS AccountService
AccountChannelS AccountChannelService AccountChannelS AccountChannelService
ChannelS ChannelService ChannelS ChannelService
ChatbotS ChatbotService
Database *sql.DB Database *sql.DB
tables []table tables []table
} }
@ -106,3 +107,12 @@ func WithChannelService() ServicesInit {
return nil return nil
} }
} }
func WithChatbotService() ServicesInit {
return func(s *Services) error {
s.ChatbotS = NewChatbotService(s.Database)
s.tables = append(s.tables, table{chatbotTable, s.ChatbotS.AutoMigrate, s.ChatbotS.DestructiveReset})
return nil
}
}

View file

@ -7,6 +7,7 @@ import (
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@ -14,13 +15,15 @@ import (
"github.com/r3labs/sse/v2" "github.com/r3labs/sse/v2"
"github.com/tylertravisty/go-utils/random" "github.com/tylertravisty/go-utils/random"
"golang.org/x/net/html"
"gopkg.in/cenkalti/backoff.v1" "gopkg.in/cenkalti/backoff.v1"
) )
type ChatInfo struct { type ChatInfo struct {
UrlPrefix string
ChatID string
ChannelID int ChannelID int
ChatID string
Page string
UrlPrefix string
} }
func (ci *ChatInfo) MessageUrl() string { func (ci *ChatInfo) MessageUrl() string {
@ -31,33 +34,37 @@ func (ci *ChatInfo) StreamUrl() string {
return fmt.Sprintf("%s/chat/%s/stream", ci.UrlPrefix, ci.ChatID) return fmt.Sprintf("%s/chat/%s/stream", ci.UrlPrefix, ci.ChatID)
} }
func (c *Client) ChatInfo() error { func (c *Client) ChatInfo(reload bool) (*ChatInfo, error) {
ci, err := c.getChatInfo() var ci *ChatInfo
if err != nil { var err error
return pkgErr("error getting chat info", err)
if c.chatInfo == nil || reload {
ci, err = c.getChatInfo()
if err != nil {
return nil, pkgErr("error getting chat info", err)
}
} }
c.chatInfo = ci c.chatInfo = ci
return nil return ci, nil
} }
func (c *Client) getChatInfo() (*ChatInfo, error) { func (c *Client) getChatInfo() (*ChatInfo, error) {
if c.StreamUrl == "" { if c.LiveStreamUrl == "" {
return nil, fmt.Errorf("stream url is empty") return nil, fmt.Errorf("stream url is empty")
} }
resp, err := c.getWebpage(c.StreamUrl) resp, err := c.getWebpage(c.LiveStreamUrl)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting stream webpage: %v", err) return nil, fmt.Errorf("error getting stream webpage: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
var chatInfo *ChatInfo
var page string
r := bufio.NewReader(resp.Body) r := bufio.NewReader(resp.Body)
//line, _, err := r.ReadLine()
lineS, err := r.ReadString('\n') lineS, err := r.ReadString('\n')
//var lineS string
for err == nil { for err == nil {
//lineS = string(line)
if strings.Contains(lineS, "RumbleChat(") { if strings.Contains(lineS, "RumbleChat(") {
start := strings.Index(lineS, "RumbleChat(") + len("RumbleChat(") start := strings.Index(lineS, "RumbleChat(") + len("RumbleChat(")
if start == -1 { if start == -1 {
@ -81,16 +88,33 @@ func (c *Client) getChatInfo() (*ChatInfo, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error converting channel ID argument string to int: %v", err) return nil, fmt.Errorf("error converting channel ID argument string to int: %v", err)
} }
return &ChatInfo{info[0], info[1], channelID}, nil chatInfo = &ChatInfo{ChannelID: channelID, ChatID: info[1], UrlPrefix: info[0]}
} else if strings.Contains(lineS, "media-by--a") && strings.Contains(lineS, "author") {
r := strings.NewReader(lineS)
node, err := html.Parse(r)
if err != nil {
return nil, fmt.Errorf("error parsing html tag with page name: %v", err)
}
if node.FirstChild != nil && node.FirstChild.LastChild != nil && node.FirstChild.LastChild.FirstChild != nil {
for _, attr := range node.FirstChild.LastChild.FirstChild.Attr {
if attr.Key == "href" {
page = attr.Val
}
}
}
} }
//line, _, err = r.ReadLine()
lineS, err = r.ReadString('\n') lineS, err = r.ReadString('\n')
} }
if err != nil { if err != nil && err != io.EOF {
return nil, fmt.Errorf("error reading line from stream webpage: %v", err) return nil, fmt.Errorf("error reading line from stream webpage: %v", err)
} }
if chatInfo == nil {
return nil, fmt.Errorf("did not find RumbleChat function call")
}
return nil, fmt.Errorf("did not find RumbleChat function call") chatInfo.Page = page
return chatInfo, nil
} }
type ChatMessage struct { type ChatMessage struct {
@ -118,20 +142,17 @@ type ChatResponse struct {
Errors []Error `json:"errors"` Errors []Error `json:"errors"`
} }
func (c *Client) Chat(asChannel bool, message string) error { func (c *Client) Chat(message string, channelID *int) error {
if c.httpClient == nil { if c.httpClient == nil {
return pkgErr("", fmt.Errorf("http client is nil")) return pkgErr("", fmt.Errorf("http client is nil"))
} }
// chatInfo, err := c.streamChatInfo()
// if err != nil {
// return pkgErr("error getting stream chat info", err)
// }
if c.chatInfo == nil { if c.chatInfo == nil {
err := c.ChatInfo() ci, err := c.getChatInfo()
if err != nil { if err != nil {
return err return pkgErr("error getting chat info", err)
} }
c.chatInfo = ci
} }
requestID, err := random.String(32) requestID, err := random.String(32)
@ -145,12 +166,12 @@ func (c *Client) Chat(asChannel bool, message string) error {
Text: message, Text: message,
}, },
Rant: nil, Rant: nil,
ChannelID: nil, ChannelID: channelID,
}, },
} }
if asChannel { // if asChannel {
body.Data.ChannelID = &c.chatInfo.ChannelID // body.Data.ChannelID = &c.chatInfo.ChannelID
} // }
bodyB, err := json.Marshal(body) bodyB, err := json.Marshal(body)
if err != nil { if err != nil {
@ -260,6 +281,14 @@ func (c *Client) StartChatStream(handle func(cv ChatView), handleError func(err
return pkgErr("", fmt.Errorf("chat stream already started")) return pkgErr("", fmt.Errorf("chat stream already started"))
} }
sseEvent := make(chan *sse.Event) sseEvent := make(chan *sse.Event)
if c.chatInfo == nil {
ci, err := c.getChatInfo()
if err != nil {
return pkgErr("error getting chat info", err)
}
c.chatInfo = ci
}
sseCl := sse.NewClient(c.chatInfo.StreamUrl()) sseCl := sse.NewClient(c.chatInfo.StreamUrl())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
sseCl.ReconnectStrategy = backoff.WithContext( sseCl.ReconnectStrategy = backoff.WithContext(

View file

@ -23,12 +23,12 @@ const (
) )
type Client struct { type Client struct {
httpClient *http.Client httpClient *http.Client
chatInfo *ChatInfo chatInfo *ChatInfo
chatStream *ChatStream chatStream *ChatStream
chatStreamMu sync.Mutex chatStreamMu sync.Mutex
StreamKey string ApiKey string
StreamUrl string LiveStreamUrl string
} }
func (c *Client) cookies() ([]*http.Cookie, error) { func (c *Client) cookies() ([]*http.Cookie, error) {
@ -53,9 +53,9 @@ func (c *Client) PrintCookies() error {
} }
type NewClientOptions struct { type NewClientOptions struct {
Cookies []*http.Cookie `json:"cookies"` Cookies []*http.Cookie `json:"cookies"`
StreamKey string `json:"stream_key"` ApiKey string `json:"stream_key"`
StreamUrl string `json:"stream_url"` LiveStreamUrl string `json:"stream_url"`
} }
func NewClient(opts NewClientOptions) (*Client, error) { func NewClient(opts NewClientOptions) (*Client, error) {
@ -64,7 +64,7 @@ func NewClient(opts NewClientOptions) (*Client, error) {
return nil, pkgErr("error creating http client", err) return nil, pkgErr("error creating http client", err)
} }
return &Client{httpClient: cl, StreamKey: opts.StreamKey, StreamUrl: opts.StreamUrl}, nil return &Client{httpClient: cl, ApiKey: opts.ApiKey, LiveStreamUrl: opts.LiveStreamUrl}, nil
} }
func newHttpClient(cookies []*http.Cookie) (*http.Client, error) { func newHttpClient(cookies []*http.Cookie) (*http.Client, error) {

View file

@ -95,7 +95,7 @@ type LivestreamResponse struct {
func (c *Client) Request() (*LivestreamResponse, error) { func (c *Client) Request() (*LivestreamResponse, error) {
hcl := http.Client{Timeout: 30 * time.Second} hcl := http.Client{Timeout: 30 * time.Second}
resp, err := hcl.Get(c.StreamKey) resp, err := hcl.Get(c.ApiKey)
if err != nil { if err != nil {
return nil, pkgErr("http Get request returned error", err) return nil, pkgErr("http Get request returned error", err)
} }

View file

@ -1564,6 +1564,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
if size > remainSize { if size > remainSize {
hdec.SetEmitEnabled(false) hdec.SetEmitEnabled(false)
mh.Truncated = true mh.Truncated = true
remainSize = 0
return return
} }
remainSize -= size remainSize -= size
@ -1576,6 +1577,36 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
var hc headersOrContinuation = hf var hc headersOrContinuation = hf
for { for {
frag := hc.HeaderBlockFragment() frag := hc.HeaderBlockFragment()
// Avoid parsing large amounts of headers that we will then discard.
// If the sender exceeds the max header list size by too much,
// skip parsing the fragment and close the connection.
//
// "Too much" is either any CONTINUATION frame after we've already
// exceeded the max header list size (in which case remainSize is 0),
// or a frame whose encoded size is more than twice the remaining
// header list bytes we're willing to accept.
if int64(len(frag)) > int64(2*remainSize) {
if VerboseLogs {
log.Printf("http2: header list too large")
}
// It would be nice to send a RST_STREAM before sending the GOAWAY,
// but the structure of the server's frame writer makes this difficult.
return nil, ConnectionError(ErrCodeProtocol)
}
// Also close the connection after any CONTINUATION frame following an
// invalid header, since we stop tracking the size of the headers after
// an invalid one.
if invalid != nil {
if VerboseLogs {
log.Printf("http2: invalid header: %v", invalid)
}
// It would be nice to send a RST_STREAM before sending the GOAWAY,
// but the structure of the server's frame writer makes this difficult.
return nil, ConnectionError(ErrCodeProtocol)
}
if _, err := hdec.Write(frag); err != nil { if _, err := hdec.Write(frag); err != nil {
return nil, ConnectionError(ErrCodeCompression) return nil, ConnectionError(ErrCodeCompression)
} }

View file

@ -77,7 +77,10 @@ func (p *pipe) Read(d []byte) (n int, err error) {
} }
} }
var errClosedPipeWrite = errors.New("write on closed buffer") var (
errClosedPipeWrite = errors.New("write on closed buffer")
errUninitializedPipeWrite = errors.New("write on uninitialized buffer")
)
// Write copies bytes from p into the buffer and wakes a reader. // Write copies bytes from p into the buffer and wakes a reader.
// It is an error to write more data than the buffer can hold. // It is an error to write more data than the buffer can hold.
@ -91,6 +94,12 @@ func (p *pipe) Write(d []byte) (n int, err error) {
if p.err != nil || p.breakErr != nil { if p.err != nil || p.breakErr != nil {
return 0, errClosedPipeWrite return 0, errClosedPipeWrite
} }
// pipe.setBuffer is never invoked, leaving the buffer uninitialized.
// We shouldn't try to write to an uninitialized pipe,
// but returning an error is better than panicking.
if p.b == nil {
return 0, errUninitializedPipeWrite
}
return p.b.Write(d) return p.b.Write(d)
} }

View file

@ -124,6 +124,7 @@ type Server struct {
// IdleTimeout specifies how long until idle clients should be // IdleTimeout specifies how long until idle clients should be
// closed with a GOAWAY frame. PING frames are not considered // closed with a GOAWAY frame. PING frames are not considered
// activity for the purposes of IdleTimeout. // activity for the purposes of IdleTimeout.
// If zero or negative, there is no timeout.
IdleTimeout time.Duration IdleTimeout time.Duration
// MaxUploadBufferPerConnection is the size of the initial flow // MaxUploadBufferPerConnection is the size of the initial flow
@ -434,7 +435,7 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) {
// passes the connection off to us with the deadline already set. // passes the connection off to us with the deadline already set.
// Write deadlines are set per stream in serverConn.newStream. // Write deadlines are set per stream in serverConn.newStream.
// Disarm the net.Conn write deadline here. // Disarm the net.Conn write deadline here.
if sc.hs.WriteTimeout != 0 { if sc.hs.WriteTimeout > 0 {
sc.conn.SetWriteDeadline(time.Time{}) sc.conn.SetWriteDeadline(time.Time{})
} }
@ -924,7 +925,7 @@ func (sc *serverConn) serve() {
sc.setConnState(http.StateActive) sc.setConnState(http.StateActive)
sc.setConnState(http.StateIdle) sc.setConnState(http.StateIdle)
if sc.srv.IdleTimeout != 0 { if sc.srv.IdleTimeout > 0 {
sc.idleTimer = time.AfterFunc(sc.srv.IdleTimeout, sc.onIdleTimer) sc.idleTimer = time.AfterFunc(sc.srv.IdleTimeout, sc.onIdleTimer)
defer sc.idleTimer.Stop() defer sc.idleTimer.Stop()
} }
@ -1637,7 +1638,7 @@ func (sc *serverConn) closeStream(st *stream, err error) {
delete(sc.streams, st.id) delete(sc.streams, st.id)
if len(sc.streams) == 0 { if len(sc.streams) == 0 {
sc.setConnState(http.StateIdle) sc.setConnState(http.StateIdle)
if sc.srv.IdleTimeout != 0 { if sc.srv.IdleTimeout > 0 {
sc.idleTimer.Reset(sc.srv.IdleTimeout) sc.idleTimer.Reset(sc.srv.IdleTimeout)
} }
if h1ServerKeepAlivesDisabled(sc.hs) { if h1ServerKeepAlivesDisabled(sc.hs) {
@ -2017,7 +2018,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
// similar to how the http1 server works. Here it's // similar to how the http1 server works. Here it's
// technically more like the http1 Server's ReadHeaderTimeout // technically more like the http1 Server's ReadHeaderTimeout
// (in Go 1.8), though. That's a more sane option anyway. // (in Go 1.8), though. That's a more sane option anyway.
if sc.hs.ReadTimeout != 0 { if sc.hs.ReadTimeout > 0 {
sc.conn.SetReadDeadline(time.Time{}) sc.conn.SetReadDeadline(time.Time{})
st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout) st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout)
} }
@ -2038,7 +2039,7 @@ func (sc *serverConn) upgradeRequest(req *http.Request) {
// Disable any read deadline set by the net/http package // Disable any read deadline set by the net/http package
// prior to the upgrade. // prior to the upgrade.
if sc.hs.ReadTimeout != 0 { if sc.hs.ReadTimeout > 0 {
sc.conn.SetReadDeadline(time.Time{}) sc.conn.SetReadDeadline(time.Time{})
} }
@ -2116,7 +2117,7 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState) *stream
st.flow.conn = &sc.flow // link to conn-level counter st.flow.conn = &sc.flow // link to conn-level counter
st.flow.add(sc.initialStreamSendWindowSize) st.flow.add(sc.initialStreamSendWindowSize)
st.inflow.init(sc.srv.initialStreamRecvWindowSize()) st.inflow.init(sc.srv.initialStreamRecvWindowSize())
if sc.hs.WriteTimeout != 0 { if sc.hs.WriteTimeout > 0 {
st.writeDeadline = time.AfterFunc(sc.hs.WriteTimeout, st.onWriteTimeout) st.writeDeadline = time.AfterFunc(sc.hs.WriteTimeout, st.onWriteTimeout)
} }

331
v1/vendor/golang.org/x/net/http2/testsync.go generated vendored Normal file
View file

@ -0,0 +1,331 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package http2
import (
"context"
"sync"
"time"
)
// testSyncHooks coordinates goroutines in tests.
//
// For example, a call to ClientConn.RoundTrip involves several goroutines, including:
// - the goroutine running RoundTrip;
// - the clientStream.doRequest goroutine, which writes the request; and
// - the clientStream.readLoop goroutine, which reads the response.
//
// Using testSyncHooks, a test can start a RoundTrip and identify when all these goroutines
// are blocked waiting for some condition such as reading the Request.Body or waiting for
// flow control to become available.
//
// The testSyncHooks also manage timers and synthetic time in tests.
// This permits us to, for example, start a request and cause it to time out waiting for
// response headers without resorting to time.Sleep calls.
type testSyncHooks struct {
// active/inactive act as a mutex and condition variable.
//
// - neither chan contains a value: testSyncHooks is locked.
// - active contains a value: unlocked, and at least one goroutine is not blocked
// - inactive contains a value: unlocked, and all goroutines are blocked
active chan struct{}
inactive chan struct{}
// goroutine counts
total int // total goroutines
condwait map[*sync.Cond]int // blocked in sync.Cond.Wait
blocked []*testBlockedGoroutine // otherwise blocked
// fake time
now time.Time
timers []*fakeTimer
// Transport testing: Report various events.
newclientconn func(*ClientConn)
newstream func(*clientStream)
}
// testBlockedGoroutine is a blocked goroutine.
type testBlockedGoroutine struct {
f func() bool // blocked until f returns true
ch chan struct{} // closed when unblocked
}
func newTestSyncHooks() *testSyncHooks {
h := &testSyncHooks{
active: make(chan struct{}, 1),
inactive: make(chan struct{}, 1),
condwait: map[*sync.Cond]int{},
}
h.inactive <- struct{}{}
h.now = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
return h
}
// lock acquires the testSyncHooks mutex.
func (h *testSyncHooks) lock() {
select {
case <-h.active:
case <-h.inactive:
}
}
// waitInactive waits for all goroutines to become inactive.
func (h *testSyncHooks) waitInactive() {
for {
<-h.inactive
if !h.unlock() {
break
}
}
}
// unlock releases the testSyncHooks mutex.
// It reports whether any goroutines are active.
func (h *testSyncHooks) unlock() (active bool) {
// Look for a blocked goroutine which can be unblocked.
blocked := h.blocked[:0]
unblocked := false
for _, b := range h.blocked {
if !unblocked && b.f() {
unblocked = true
close(b.ch)
} else {
blocked = append(blocked, b)
}
}
h.blocked = blocked
// Count goroutines blocked on condition variables.
condwait := 0
for _, count := range h.condwait {
condwait += count
}
if h.total > condwait+len(blocked) {
h.active <- struct{}{}
return true
} else {
h.inactive <- struct{}{}
return false
}
}
// goRun starts a new goroutine.
func (h *testSyncHooks) goRun(f func()) {
h.lock()
h.total++
h.unlock()
go func() {
defer func() {
h.lock()
h.total--
h.unlock()
}()
f()
}()
}
// blockUntil indicates that a goroutine is blocked waiting for some condition to become true.
// It waits until f returns true before proceeding.
//
// Example usage:
//
// h.blockUntil(func() bool {
// // Is the context done yet?
// select {
// case <-ctx.Done():
// default:
// return false
// }
// return true
// })
// // Wait for the context to become done.
// <-ctx.Done()
//
// The function f passed to blockUntil must be non-blocking and idempotent.
func (h *testSyncHooks) blockUntil(f func() bool) {
if f() {
return
}
ch := make(chan struct{})
h.lock()
h.blocked = append(h.blocked, &testBlockedGoroutine{
f: f,
ch: ch,
})
h.unlock()
<-ch
}
// broadcast is sync.Cond.Broadcast.
func (h *testSyncHooks) condBroadcast(cond *sync.Cond) {
h.lock()
delete(h.condwait, cond)
h.unlock()
cond.Broadcast()
}
// broadcast is sync.Cond.Wait.
func (h *testSyncHooks) condWait(cond *sync.Cond) {
h.lock()
h.condwait[cond]++
h.unlock()
}
// newTimer creates a new fake timer.
func (h *testSyncHooks) newTimer(d time.Duration) timer {
h.lock()
defer h.unlock()
t := &fakeTimer{
hooks: h,
when: h.now.Add(d),
c: make(chan time.Time),
}
h.timers = append(h.timers, t)
return t
}
// afterFunc creates a new fake AfterFunc timer.
func (h *testSyncHooks) afterFunc(d time.Duration, f func()) timer {
h.lock()
defer h.unlock()
t := &fakeTimer{
hooks: h,
when: h.now.Add(d),
f: f,
}
h.timers = append(h.timers, t)
return t
}
func (h *testSyncHooks) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
t := h.afterFunc(d, cancel)
return ctx, func() {
t.Stop()
cancel()
}
}
func (h *testSyncHooks) timeUntilEvent() time.Duration {
h.lock()
defer h.unlock()
var next time.Time
for _, t := range h.timers {
if next.IsZero() || t.when.Before(next) {
next = t.when
}
}
if d := next.Sub(h.now); d > 0 {
return d
}
return 0
}
// advance advances time and causes synthetic timers to fire.
func (h *testSyncHooks) advance(d time.Duration) {
h.lock()
defer h.unlock()
h.now = h.now.Add(d)
timers := h.timers[:0]
for _, t := range h.timers {
t := t // remove after go.mod depends on go1.22
t.mu.Lock()
switch {
case t.when.After(h.now):
timers = append(timers, t)
case t.when.IsZero():
// stopped timer
default:
t.when = time.Time{}
if t.c != nil {
close(t.c)
}
if t.f != nil {
h.total++
go func() {
defer func() {
h.lock()
h.total--
h.unlock()
}()
t.f()
}()
}
}
t.mu.Unlock()
}
h.timers = timers
}
// A timer wraps a time.Timer, or a synthetic equivalent in tests.
// Unlike time.Timer, timer is single-use: The timer channel is closed when the timer expires.
type timer interface {
C() <-chan time.Time
Stop() bool
Reset(d time.Duration) bool
}
// timeTimer implements timer using real time.
type timeTimer struct {
t *time.Timer
c chan time.Time
}
// newTimeTimer creates a new timer using real time.
func newTimeTimer(d time.Duration) timer {
ch := make(chan time.Time)
t := time.AfterFunc(d, func() {
close(ch)
})
return &timeTimer{t, ch}
}
// newTimeAfterFunc creates an AfterFunc timer using real time.
func newTimeAfterFunc(d time.Duration, f func()) timer {
return &timeTimer{
t: time.AfterFunc(d, f),
}
}
func (t timeTimer) C() <-chan time.Time { return t.c }
func (t timeTimer) Stop() bool { return t.t.Stop() }
func (t timeTimer) Reset(d time.Duration) bool { return t.t.Reset(d) }
// fakeTimer implements timer using fake time.
type fakeTimer struct {
hooks *testSyncHooks
mu sync.Mutex
when time.Time // when the timer will fire
c chan time.Time // closed when the timer fires; mutually exclusive with f
f func() // called when the timer fires; mutually exclusive with c
}
func (t *fakeTimer) C() <-chan time.Time { return t.c }
func (t *fakeTimer) Stop() bool {
t.mu.Lock()
defer t.mu.Unlock()
stopped := t.when.IsZero()
t.when = time.Time{}
return stopped
}
func (t *fakeTimer) Reset(d time.Duration) bool {
if t.c != nil || t.f == nil {
panic("fakeTimer only supports Reset on AfterFunc timers")
}
t.mu.Lock()
defer t.mu.Unlock()
t.hooks.lock()
defer t.hooks.unlock()
active := !t.when.IsZero()
t.when = t.hooks.now.Add(d)
if !active {
t.hooks.timers = append(t.hooks.timers, t)
}
return active
}

View file

@ -147,6 +147,12 @@ type Transport struct {
// waiting for their turn. // waiting for their turn.
StrictMaxConcurrentStreams bool StrictMaxConcurrentStreams bool
// IdleConnTimeout is the maximum amount of time an idle
// (keep-alive) connection will remain idle before closing
// itself.
// Zero means no limit.
IdleConnTimeout time.Duration
// ReadIdleTimeout is the timeout after which a health check using ping // ReadIdleTimeout is the timeout after which a health check using ping
// frame will be carried out if no frame is received on the connection. // frame will be carried out if no frame is received on the connection.
// Note that a ping response will is considered a received frame, so if // Note that a ping response will is considered a received frame, so if
@ -178,6 +184,8 @@ type Transport struct {
connPoolOnce sync.Once connPoolOnce sync.Once
connPoolOrDef ClientConnPool // non-nil version of ConnPool connPoolOrDef ClientConnPool // non-nil version of ConnPool
syncHooks *testSyncHooks
} }
func (t *Transport) maxHeaderListSize() uint32 { func (t *Transport) maxHeaderListSize() uint32 {
@ -302,7 +310,7 @@ type ClientConn struct {
readerErr error // set before readerDone is closed readerErr error // set before readerDone is closed
idleTimeout time.Duration // or 0 for never idleTimeout time.Duration // or 0 for never
idleTimer *time.Timer idleTimer timer
mu sync.Mutex // guards following mu sync.Mutex // guards following
cond *sync.Cond // hold mu; broadcast on flow/closed changes cond *sync.Cond // hold mu; broadcast on flow/closed changes
@ -344,6 +352,60 @@ type ClientConn struct {
werr error // first write error that has occurred werr error // first write error that has occurred
hbuf bytes.Buffer // HPACK encoder writes into this hbuf bytes.Buffer // HPACK encoder writes into this
henc *hpack.Encoder henc *hpack.Encoder
syncHooks *testSyncHooks // can be nil
}
// Hook points used for testing.
// Outside of tests, cc.syncHooks is nil and these all have minimal implementations.
// Inside tests, see the testSyncHooks function docs.
// goRun starts a new goroutine.
func (cc *ClientConn) goRun(f func()) {
if cc.syncHooks != nil {
cc.syncHooks.goRun(f)
return
}
go f()
}
// condBroadcast is cc.cond.Broadcast.
func (cc *ClientConn) condBroadcast() {
if cc.syncHooks != nil {
cc.syncHooks.condBroadcast(cc.cond)
}
cc.cond.Broadcast()
}
// condWait is cc.cond.Wait.
func (cc *ClientConn) condWait() {
if cc.syncHooks != nil {
cc.syncHooks.condWait(cc.cond)
}
cc.cond.Wait()
}
// newTimer creates a new time.Timer, or a synthetic timer in tests.
func (cc *ClientConn) newTimer(d time.Duration) timer {
if cc.syncHooks != nil {
return cc.syncHooks.newTimer(d)
}
return newTimeTimer(d)
}
// afterFunc creates a new time.AfterFunc timer, or a synthetic timer in tests.
func (cc *ClientConn) afterFunc(d time.Duration, f func()) timer {
if cc.syncHooks != nil {
return cc.syncHooks.afterFunc(d, f)
}
return newTimeAfterFunc(d, f)
}
func (cc *ClientConn) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
if cc.syncHooks != nil {
return cc.syncHooks.contextWithTimeout(ctx, d)
}
return context.WithTimeout(ctx, d)
} }
// clientStream is the state for a single HTTP/2 stream. One of these // clientStream is the state for a single HTTP/2 stream. One of these
@ -425,7 +487,7 @@ func (cs *clientStream) abortStreamLocked(err error) {
// TODO(dneil): Clean up tests where cs.cc.cond is nil. // TODO(dneil): Clean up tests where cs.cc.cond is nil.
if cs.cc.cond != nil { if cs.cc.cond != nil {
// Wake up writeRequestBody if it is waiting on flow control. // Wake up writeRequestBody if it is waiting on flow control.
cs.cc.cond.Broadcast() cs.cc.condBroadcast()
} }
} }
@ -435,7 +497,7 @@ func (cs *clientStream) abortRequestBodyWrite() {
defer cc.mu.Unlock() defer cc.mu.Unlock()
if cs.reqBody != nil && cs.reqBodyClosed == nil { if cs.reqBody != nil && cs.reqBodyClosed == nil {
cs.closeReqBodyLocked() cs.closeReqBodyLocked()
cc.cond.Broadcast() cc.condBroadcast()
} }
} }
@ -445,10 +507,10 @@ func (cs *clientStream) closeReqBodyLocked() {
} }
cs.reqBodyClosed = make(chan struct{}) cs.reqBodyClosed = make(chan struct{})
reqBodyClosed := cs.reqBodyClosed reqBodyClosed := cs.reqBodyClosed
go func() { cs.cc.goRun(func() {
cs.reqBody.Close() cs.reqBody.Close()
close(reqBodyClosed) close(reqBodyClosed)
}() })
} }
type stickyErrWriter struct { type stickyErrWriter struct {
@ -537,15 +599,6 @@ func authorityAddr(scheme string, authority string) (addr string) {
return net.JoinHostPort(host, port) return net.JoinHostPort(host, port)
} }
var retryBackoffHook func(time.Duration) *time.Timer
func backoffNewTimer(d time.Duration) *time.Timer {
if retryBackoffHook != nil {
return retryBackoffHook(d)
}
return time.NewTimer(d)
}
// RoundTripOpt is like RoundTrip, but takes options. // RoundTripOpt is like RoundTrip, but takes options.
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) { func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) { if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) {
@ -573,13 +626,27 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
backoff := float64(uint(1) << (uint(retry) - 1)) backoff := float64(uint(1) << (uint(retry) - 1))
backoff += backoff * (0.1 * mathrand.Float64()) backoff += backoff * (0.1 * mathrand.Float64())
d := time.Second * time.Duration(backoff) d := time.Second * time.Duration(backoff)
timer := backoffNewTimer(d) var tm timer
if t.syncHooks != nil {
tm = t.syncHooks.newTimer(d)
t.syncHooks.blockUntil(func() bool {
select {
case <-tm.C():
case <-req.Context().Done():
default:
return false
}
return true
})
} else {
tm = newTimeTimer(d)
}
select { select {
case <-timer.C: case <-tm.C():
t.vlogf("RoundTrip retrying after failure: %v", roundTripErr) t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
continue continue
case <-req.Context().Done(): case <-req.Context().Done():
timer.Stop() tm.Stop()
err = req.Context().Err() err = req.Context().Err()
} }
} }
@ -658,6 +725,9 @@ func canRetryError(err error) bool {
} }
func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) { func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) {
if t.syncHooks != nil {
return t.newClientConn(nil, singleUse, t.syncHooks)
}
host, _, err := net.SplitHostPort(addr) host, _, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -666,7 +736,7 @@ func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse b
if err != nil { if err != nil {
return nil, err return nil, err
} }
return t.newClientConn(tconn, singleUse) return t.newClientConn(tconn, singleUse, nil)
} }
func (t *Transport) newTLSConfig(host string) *tls.Config { func (t *Transport) newTLSConfig(host string) *tls.Config {
@ -732,10 +802,10 @@ func (t *Transport) maxEncoderHeaderTableSize() uint32 {
} }
func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) { func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) {
return t.newClientConn(c, t.disableKeepAlives()) return t.newClientConn(c, t.disableKeepAlives(), nil)
} }
func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) { func (t *Transport) newClientConn(c net.Conn, singleUse bool, hooks *testSyncHooks) (*ClientConn, error) {
cc := &ClientConn{ cc := &ClientConn{
t: t, t: t,
tconn: c, tconn: c,
@ -750,10 +820,15 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
wantSettingsAck: true, wantSettingsAck: true,
pings: make(map[[8]byte]chan struct{}), pings: make(map[[8]byte]chan struct{}),
reqHeaderMu: make(chan struct{}, 1), reqHeaderMu: make(chan struct{}, 1),
syncHooks: hooks,
}
if hooks != nil {
hooks.newclientconn(cc)
c = cc.tconn
} }
if d := t.idleConnTimeout(); d != 0 { if d := t.idleConnTimeout(); d != 0 {
cc.idleTimeout = d cc.idleTimeout = d
cc.idleTimer = time.AfterFunc(d, cc.onIdleTimeout) cc.idleTimer = cc.afterFunc(d, cc.onIdleTimeout)
} }
if VerboseLogs { if VerboseLogs {
t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr()) t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr())
@ -818,7 +893,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
return nil, cc.werr return nil, cc.werr
} }
go cc.readLoop() cc.goRun(cc.readLoop)
return cc, nil return cc, nil
} }
@ -826,7 +901,7 @@ func (cc *ClientConn) healthCheck() {
pingTimeout := cc.t.pingTimeout() pingTimeout := cc.t.pingTimeout()
// We don't need to periodically ping in the health check, because the readLoop of ClientConn will // We don't need to periodically ping in the health check, because the readLoop of ClientConn will
// trigger the healthCheck again if there is no frame received. // trigger the healthCheck again if there is no frame received.
ctx, cancel := context.WithTimeout(context.Background(), pingTimeout) ctx, cancel := cc.contextWithTimeout(context.Background(), pingTimeout)
defer cancel() defer cancel()
cc.vlogf("http2: Transport sending health check") cc.vlogf("http2: Transport sending health check")
err := cc.Ping(ctx) err := cc.Ping(ctx)
@ -1056,7 +1131,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
// Wait for all in-flight streams to complete or connection to close // Wait for all in-flight streams to complete or connection to close
done := make(chan struct{}) done := make(chan struct{})
cancelled := false // guarded by cc.mu cancelled := false // guarded by cc.mu
go func() { cc.goRun(func() {
cc.mu.Lock() cc.mu.Lock()
defer cc.mu.Unlock() defer cc.mu.Unlock()
for { for {
@ -1068,9 +1143,9 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
if cancelled { if cancelled {
break break
} }
cc.cond.Wait() cc.condWait()
} }
}() })
shutdownEnterWaitStateHook() shutdownEnterWaitStateHook()
select { select {
case <-done: case <-done:
@ -1080,7 +1155,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
cc.mu.Lock() cc.mu.Lock()
// Free the goroutine above // Free the goroutine above
cancelled = true cancelled = true
cc.cond.Broadcast() cc.condBroadcast()
cc.mu.Unlock() cc.mu.Unlock()
return ctx.Err() return ctx.Err()
} }
@ -1118,7 +1193,7 @@ func (cc *ClientConn) closeForError(err error) {
for _, cs := range cc.streams { for _, cs := range cc.streams {
cs.abortStreamLocked(err) cs.abortStreamLocked(err)
} }
cc.cond.Broadcast() cc.condBroadcast()
cc.mu.Unlock() cc.mu.Unlock()
cc.closeConn() cc.closeConn()
} }
@ -1215,6 +1290,10 @@ func (cc *ClientConn) decrStreamReservationsLocked() {
} }
func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
return cc.roundTrip(req, nil)
}
func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream)) (*http.Response, error) {
ctx := req.Context() ctx := req.Context()
cs := &clientStream{ cs := &clientStream{
cc: cc, cc: cc,
@ -1229,9 +1308,23 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
respHeaderRecv: make(chan struct{}), respHeaderRecv: make(chan struct{}),
donec: make(chan struct{}), donec: make(chan struct{}),
} }
go cs.doRequest(req) cc.goRun(func() {
cs.doRequest(req)
})
waitDone := func() error { waitDone := func() error {
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-cs.donec:
case <-ctx.Done():
case <-cs.reqCancel:
default:
return false
}
return true
})
}
select { select {
case <-cs.donec: case <-cs.donec:
return nil return nil
@ -1292,7 +1385,24 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
return err return err
} }
if streamf != nil {
streamf(cs)
}
for { for {
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-cs.respHeaderRecv:
case <-cs.abort:
case <-ctx.Done():
case <-cs.reqCancel:
default:
return false
}
return true
})
}
select { select {
case <-cs.respHeaderRecv: case <-cs.respHeaderRecv:
return handleResponseHeaders() return handleResponseHeaders()
@ -1348,6 +1458,21 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) {
if cc.reqHeaderMu == nil { if cc.reqHeaderMu == nil {
panic("RoundTrip on uninitialized ClientConn") // for tests panic("RoundTrip on uninitialized ClientConn") // for tests
} }
var newStreamHook func(*clientStream)
if cc.syncHooks != nil {
newStreamHook = cc.syncHooks.newstream
cc.syncHooks.blockUntil(func() bool {
select {
case cc.reqHeaderMu <- struct{}{}:
<-cc.reqHeaderMu
case <-cs.reqCancel:
case <-ctx.Done():
default:
return false
}
return true
})
}
select { select {
case cc.reqHeaderMu <- struct{}{}: case cc.reqHeaderMu <- struct{}{}:
case <-cs.reqCancel: case <-cs.reqCancel:
@ -1372,6 +1497,10 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) {
} }
cc.mu.Unlock() cc.mu.Unlock()
if newStreamHook != nil {
newStreamHook(cs)
}
// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
if !cc.t.disableCompression() && if !cc.t.disableCompression() &&
req.Header.Get("Accept-Encoding") == "" && req.Header.Get("Accept-Encoding") == "" &&
@ -1452,15 +1581,30 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) {
var respHeaderTimer <-chan time.Time var respHeaderTimer <-chan time.Time
var respHeaderRecv chan struct{} var respHeaderRecv chan struct{}
if d := cc.responseHeaderTimeout(); d != 0 { if d := cc.responseHeaderTimeout(); d != 0 {
timer := time.NewTimer(d) timer := cc.newTimer(d)
defer timer.Stop() defer timer.Stop()
respHeaderTimer = timer.C respHeaderTimer = timer.C()
respHeaderRecv = cs.respHeaderRecv respHeaderRecv = cs.respHeaderRecv
} }
// Wait until the peer half-closes its end of the stream, // Wait until the peer half-closes its end of the stream,
// or until the request is aborted (via context, error, or otherwise), // or until the request is aborted (via context, error, or otherwise),
// whichever comes first. // whichever comes first.
for { for {
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-cs.peerClosed:
case <-respHeaderTimer:
case <-respHeaderRecv:
case <-cs.abort:
case <-ctx.Done():
case <-cs.reqCancel:
default:
return false
}
return true
})
}
select { select {
case <-cs.peerClosed: case <-cs.peerClosed:
return nil return nil
@ -1609,7 +1753,7 @@ func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error {
return nil return nil
} }
cc.pendingRequests++ cc.pendingRequests++
cc.cond.Wait() cc.condWait()
cc.pendingRequests-- cc.pendingRequests--
select { select {
case <-cs.abort: case <-cs.abort:
@ -1871,10 +2015,26 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
cs.flow.take(take) cs.flow.take(take)
return take, nil return take, nil
} }
cc.cond.Wait() cc.condWait()
} }
} }
func validateHeaders(hdrs http.Header) string {
for k, vv := range hdrs {
if !httpguts.ValidHeaderFieldName(k) {
return fmt.Sprintf("name %q", k)
}
for _, v := range vv {
if !httpguts.ValidHeaderFieldValue(v) {
// Don't include the value in the error,
// because it may be sensitive.
return fmt.Sprintf("value for header %q", k)
}
}
}
return ""
}
var errNilRequestURL = errors.New("http2: Request.URI is nil") var errNilRequestURL = errors.New("http2: Request.URI is nil")
// requires cc.wmu be held. // requires cc.wmu be held.
@ -1912,19 +2072,14 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
} }
} }
// Check for any invalid headers and return an error before we // Check for any invalid headers+trailers and return an error before we
// potentially pollute our hpack state. (We want to be able to // potentially pollute our hpack state. (We want to be able to
// continue to reuse the hpack encoder for future requests) // continue to reuse the hpack encoder for future requests)
for k, vv := range req.Header { if err := validateHeaders(req.Header); err != "" {
if !httpguts.ValidHeaderFieldName(k) { return nil, fmt.Errorf("invalid HTTP header %s", err)
return nil, fmt.Errorf("invalid HTTP header name %q", k) }
} if err := validateHeaders(req.Trailer); err != "" {
for _, v := range vv { return nil, fmt.Errorf("invalid HTTP trailer %s", err)
if !httpguts.ValidHeaderFieldValue(v) {
// Don't include the value in the error, because it may be sensitive.
return nil, fmt.Errorf("invalid HTTP header value for header %q", k)
}
}
} }
enumerateHeaders := func(f func(name, value string)) { enumerateHeaders := func(f func(name, value string)) {
@ -2143,7 +2298,7 @@ func (cc *ClientConn) forgetStreamID(id uint32) {
} }
// Wake up writeRequestBody via clientStream.awaitFlowControl and // Wake up writeRequestBody via clientStream.awaitFlowControl and
// wake up RoundTrip if there is a pending request. // wake up RoundTrip if there is a pending request.
cc.cond.Broadcast() cc.condBroadcast()
closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil
if closeOnIdle && cc.streamsReserved == 0 && len(cc.streams) == 0 { if closeOnIdle && cc.streamsReserved == 0 && len(cc.streams) == 0 {
@ -2231,7 +2386,7 @@ func (rl *clientConnReadLoop) cleanup() {
cs.abortStreamLocked(err) cs.abortStreamLocked(err)
} }
} }
cc.cond.Broadcast() cc.condBroadcast()
cc.mu.Unlock() cc.mu.Unlock()
} }
@ -2266,10 +2421,9 @@ func (rl *clientConnReadLoop) run() error {
cc := rl.cc cc := rl.cc
gotSettings := false gotSettings := false
readIdleTimeout := cc.t.ReadIdleTimeout readIdleTimeout := cc.t.ReadIdleTimeout
var t *time.Timer var t timer
if readIdleTimeout != 0 { if readIdleTimeout != 0 {
t = time.AfterFunc(readIdleTimeout, cc.healthCheck) t = cc.afterFunc(readIdleTimeout, cc.healthCheck)
defer t.Stop()
} }
for { for {
f, err := cc.fr.ReadFrame() f, err := cc.fr.ReadFrame()
@ -2684,7 +2838,7 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error {
}) })
return nil return nil
} }
if !cs.firstByte { if !cs.pastHeaders {
cc.logf("protocol error: received DATA before a HEADERS frame") cc.logf("protocol error: received DATA before a HEADERS frame")
rl.endStreamError(cs, StreamError{ rl.endStreamError(cs, StreamError{
StreamID: f.StreamID, StreamID: f.StreamID,
@ -2867,7 +3021,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
for _, cs := range cc.streams { for _, cs := range cc.streams {
cs.flow.add(delta) cs.flow.add(delta)
} }
cc.cond.Broadcast() cc.condBroadcast()
cc.initialWindowSize = s.Val cc.initialWindowSize = s.Val
case SettingHeaderTableSize: case SettingHeaderTableSize:
@ -2922,7 +3076,7 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
return ConnectionError(ErrCodeFlowControl) return ConnectionError(ErrCodeFlowControl)
} }
cc.cond.Broadcast() cc.condBroadcast()
return nil return nil
} }
@ -2964,24 +3118,38 @@ func (cc *ClientConn) Ping(ctx context.Context) error {
} }
cc.mu.Unlock() cc.mu.Unlock()
} }
errc := make(chan error, 1) var pingError error
go func() { errc := make(chan struct{})
cc.goRun(func() {
cc.wmu.Lock() cc.wmu.Lock()
defer cc.wmu.Unlock() defer cc.wmu.Unlock()
if err := cc.fr.WritePing(false, p); err != nil { if pingError = cc.fr.WritePing(false, p); pingError != nil {
errc <- err close(errc)
return return
} }
if err := cc.bw.Flush(); err != nil { if pingError = cc.bw.Flush(); pingError != nil {
errc <- err close(errc)
return return
} }
}() })
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-c:
case <-errc:
case <-ctx.Done():
case <-cc.readerDone:
default:
return false
}
return true
})
}
select { select {
case <-c: case <-c:
return nil return nil
case err := <-errc: case <-errc:
return err return pingError
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case <-cc.readerDone: case <-cc.readerDone:
@ -3150,9 +3318,17 @@ func (rt noDialH2RoundTripper) RoundTrip(req *http.Request) (*http.Response, err
} }
func (t *Transport) idleConnTimeout() time.Duration { func (t *Transport) idleConnTimeout() time.Duration {
// to keep things backwards compatible, we use non-zero values of
// IdleConnTimeout, followed by using the IdleConnTimeout on the underlying
// http1 transport, followed by 0
if t.IdleConnTimeout != 0 {
return t.IdleConnTimeout
}
if t.t1 != nil { if t.t1 != nil {
return t.t1.IdleConnTimeout return t.t1.IdleConnTimeout
} }
return 0 return 0
} }

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build aix || darwin || dragonfly || freebsd || openbsd || solaris //go:build aix || darwin || dragonfly || freebsd || openbsd || solaris || zos
package unix package unix

View file

@ -1520,6 +1520,14 @@ func (m *mmapper) Munmap(data []byte) (err error) {
return nil return nil
} }
func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error) {
return mapper.Mmap(fd, offset, length, prot, flags)
}
func Munmap(b []byte) (err error) {
return mapper.Munmap(b)
}
func Read(fd int, p []byte) (n int, err error) { func Read(fd int, p []byte) (n int, err error) {
n, err = read(fd, p) n, err = read(fd, p)
if raceenabled { if raceenabled {

View file

@ -165,6 +165,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
//sys CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile Handle) (handle Handle, err error) [failretval==InvalidHandle] = CreateFileW //sys CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile Handle) (handle Handle, err error) [failretval==InvalidHandle] = CreateFileW
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *SecurityAttributes) (handle Handle, err error) [failretval==InvalidHandle] = CreateNamedPipeW //sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *SecurityAttributes) (handle Handle, err error) [failretval==InvalidHandle] = CreateNamedPipeW
//sys ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error) //sys ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error)
//sys DisconnectNamedPipe(pipe Handle) (err error)
//sys GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) //sys GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error)
//sys GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW //sys GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
//sys SetNamedPipeHandleState(pipe Handle, state *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32) (err error) = SetNamedPipeHandleState //sys SetNamedPipeHandleState(pipe Handle, state *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32) (err error) = SetNamedPipeHandleState
@ -348,8 +349,19 @@ func NewCallbackCDecl(fn interface{}) uintptr {
//sys SetProcessPriorityBoost(process Handle, disable bool) (err error) = kernel32.SetProcessPriorityBoost //sys SetProcessPriorityBoost(process Handle, disable bool) (err error) = kernel32.SetProcessPriorityBoost
//sys GetProcessWorkingSetSizeEx(hProcess Handle, lpMinimumWorkingSetSize *uintptr, lpMaximumWorkingSetSize *uintptr, flags *uint32) //sys GetProcessWorkingSetSizeEx(hProcess Handle, lpMinimumWorkingSetSize *uintptr, lpMaximumWorkingSetSize *uintptr, flags *uint32)
//sys SetProcessWorkingSetSizeEx(hProcess Handle, dwMinimumWorkingSetSize uintptr, dwMaximumWorkingSetSize uintptr, flags uint32) (err error) //sys SetProcessWorkingSetSizeEx(hProcess Handle, dwMinimumWorkingSetSize uintptr, dwMaximumWorkingSetSize uintptr, flags uint32) (err error)
//sys ClearCommBreak(handle Handle) (err error)
//sys ClearCommError(handle Handle, lpErrors *uint32, lpStat *ComStat) (err error)
//sys EscapeCommFunction(handle Handle, dwFunc uint32) (err error)
//sys GetCommState(handle Handle, lpDCB *DCB) (err error)
//sys GetCommModemStatus(handle Handle, lpModemStat *uint32) (err error)
//sys GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) //sys GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
//sys PurgeComm(handle Handle, dwFlags uint32) (err error)
//sys SetCommBreak(handle Handle) (err error)
//sys SetCommMask(handle Handle, dwEvtMask uint32) (err error)
//sys SetCommState(handle Handle, lpDCB *DCB) (err error)
//sys SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) //sys SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
//sys SetupComm(handle Handle, dwInQueue uint32, dwOutQueue uint32) (err error)
//sys WaitCommEvent(handle Handle, lpEvtMask *uint32, lpOverlapped *Overlapped) (err error)
//sys GetActiveProcessorCount(groupNumber uint16) (ret uint32) //sys GetActiveProcessorCount(groupNumber uint16) (ret uint32)
//sys GetMaximumProcessorCount(groupNumber uint16) (ret uint32) //sys GetMaximumProcessorCount(groupNumber uint16) (ret uint32)
//sys EnumWindows(enumFunc uintptr, param unsafe.Pointer) (err error) = user32.EnumWindows //sys EnumWindows(enumFunc uintptr, param unsafe.Pointer) (err error) = user32.EnumWindows
@ -1834,3 +1846,73 @@ func ResizePseudoConsole(pconsole Handle, size Coord) error {
// accept arguments that can be casted to uintptr, and Coord can't. // accept arguments that can be casted to uintptr, and Coord can't.
return resizePseudoConsole(pconsole, *((*uint32)(unsafe.Pointer(&size)))) return resizePseudoConsole(pconsole, *((*uint32)(unsafe.Pointer(&size))))
} }
// DCB constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-dcb.
const (
CBR_110 = 110
CBR_300 = 300
CBR_600 = 600
CBR_1200 = 1200
CBR_2400 = 2400
CBR_4800 = 4800
CBR_9600 = 9600
CBR_14400 = 14400
CBR_19200 = 19200
CBR_38400 = 38400
CBR_57600 = 57600
CBR_115200 = 115200
CBR_128000 = 128000
CBR_256000 = 256000
DTR_CONTROL_DISABLE = 0x00000000
DTR_CONTROL_ENABLE = 0x00000010
DTR_CONTROL_HANDSHAKE = 0x00000020
RTS_CONTROL_DISABLE = 0x00000000
RTS_CONTROL_ENABLE = 0x00001000
RTS_CONTROL_HANDSHAKE = 0x00002000
RTS_CONTROL_TOGGLE = 0x00003000
NOPARITY = 0
ODDPARITY = 1
EVENPARITY = 2
MARKPARITY = 3
SPACEPARITY = 4
ONESTOPBIT = 0
ONE5STOPBITS = 1
TWOSTOPBITS = 2
)
// EscapeCommFunction constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-escapecommfunction.
const (
SETXOFF = 1
SETXON = 2
SETRTS = 3
CLRRTS = 4
SETDTR = 5
CLRDTR = 6
SETBREAK = 8
CLRBREAK = 9
)
// PurgeComm constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-purgecomm.
const (
PURGE_TXABORT = 0x0001
PURGE_RXABORT = 0x0002
PURGE_TXCLEAR = 0x0004
PURGE_RXCLEAR = 0x0008
)
// SetCommMask constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setcommmask.
const (
EV_RXCHAR = 0x0001
EV_RXFLAG = 0x0002
EV_TXEMPTY = 0x0004
EV_CTS = 0x0008
EV_DSR = 0x0010
EV_RLSD = 0x0020
EV_BREAK = 0x0040
EV_ERR = 0x0080
EV_RING = 0x0100
)

View file

@ -3380,3 +3380,27 @@ type BLOB struct {
Size uint32 Size uint32
BlobData *byte BlobData *byte
} }
type ComStat struct {
Flags uint32
CBInQue uint32
CBOutQue uint32
}
type DCB struct {
DCBlength uint32
BaudRate uint32
Flags uint32
wReserved uint16
XonLim uint16
XoffLim uint16
ByteSize uint8
Parity uint8
StopBits uint8
XonChar byte
XoffChar byte
ErrorChar byte
EofChar byte
EvtChar byte
wReserved1 uint16
}

View file

@ -188,6 +188,8 @@ var (
procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject") procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject")
procCancelIo = modkernel32.NewProc("CancelIo") procCancelIo = modkernel32.NewProc("CancelIo")
procCancelIoEx = modkernel32.NewProc("CancelIoEx") procCancelIoEx = modkernel32.NewProc("CancelIoEx")
procClearCommBreak = modkernel32.NewProc("ClearCommBreak")
procClearCommError = modkernel32.NewProc("ClearCommError")
procCloseHandle = modkernel32.NewProc("CloseHandle") procCloseHandle = modkernel32.NewProc("CloseHandle")
procClosePseudoConsole = modkernel32.NewProc("ClosePseudoConsole") procClosePseudoConsole = modkernel32.NewProc("ClosePseudoConsole")
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe") procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
@ -212,7 +214,9 @@ var (
procDeleteProcThreadAttributeList = modkernel32.NewProc("DeleteProcThreadAttributeList") procDeleteProcThreadAttributeList = modkernel32.NewProc("DeleteProcThreadAttributeList")
procDeleteVolumeMountPointW = modkernel32.NewProc("DeleteVolumeMountPointW") procDeleteVolumeMountPointW = modkernel32.NewProc("DeleteVolumeMountPointW")
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl") procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe")
procDuplicateHandle = modkernel32.NewProc("DuplicateHandle") procDuplicateHandle = modkernel32.NewProc("DuplicateHandle")
procEscapeCommFunction = modkernel32.NewProc("EscapeCommFunction")
procExitProcess = modkernel32.NewProc("ExitProcess") procExitProcess = modkernel32.NewProc("ExitProcess")
procExpandEnvironmentStringsW = modkernel32.NewProc("ExpandEnvironmentStringsW") procExpandEnvironmentStringsW = modkernel32.NewProc("ExpandEnvironmentStringsW")
procFindClose = modkernel32.NewProc("FindClose") procFindClose = modkernel32.NewProc("FindClose")
@ -236,6 +240,8 @@ var (
procGenerateConsoleCtrlEvent = modkernel32.NewProc("GenerateConsoleCtrlEvent") procGenerateConsoleCtrlEvent = modkernel32.NewProc("GenerateConsoleCtrlEvent")
procGetACP = modkernel32.NewProc("GetACP") procGetACP = modkernel32.NewProc("GetACP")
procGetActiveProcessorCount = modkernel32.NewProc("GetActiveProcessorCount") procGetActiveProcessorCount = modkernel32.NewProc("GetActiveProcessorCount")
procGetCommModemStatus = modkernel32.NewProc("GetCommModemStatus")
procGetCommState = modkernel32.NewProc("GetCommState")
procGetCommTimeouts = modkernel32.NewProc("GetCommTimeouts") procGetCommTimeouts = modkernel32.NewProc("GetCommTimeouts")
procGetCommandLineW = modkernel32.NewProc("GetCommandLineW") procGetCommandLineW = modkernel32.NewProc("GetCommandLineW")
procGetComputerNameExW = modkernel32.NewProc("GetComputerNameExW") procGetComputerNameExW = modkernel32.NewProc("GetComputerNameExW")
@ -322,6 +328,7 @@ var (
procProcess32NextW = modkernel32.NewProc("Process32NextW") procProcess32NextW = modkernel32.NewProc("Process32NextW")
procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId") procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId")
procPulseEvent = modkernel32.NewProc("PulseEvent") procPulseEvent = modkernel32.NewProc("PulseEvent")
procPurgeComm = modkernel32.NewProc("PurgeComm")
procQueryDosDeviceW = modkernel32.NewProc("QueryDosDeviceW") procQueryDosDeviceW = modkernel32.NewProc("QueryDosDeviceW")
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW") procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
procQueryInformationJobObject = modkernel32.NewProc("QueryInformationJobObject") procQueryInformationJobObject = modkernel32.NewProc("QueryInformationJobObject")
@ -335,6 +342,9 @@ var (
procResetEvent = modkernel32.NewProc("ResetEvent") procResetEvent = modkernel32.NewProc("ResetEvent")
procResizePseudoConsole = modkernel32.NewProc("ResizePseudoConsole") procResizePseudoConsole = modkernel32.NewProc("ResizePseudoConsole")
procResumeThread = modkernel32.NewProc("ResumeThread") procResumeThread = modkernel32.NewProc("ResumeThread")
procSetCommBreak = modkernel32.NewProc("SetCommBreak")
procSetCommMask = modkernel32.NewProc("SetCommMask")
procSetCommState = modkernel32.NewProc("SetCommState")
procSetCommTimeouts = modkernel32.NewProc("SetCommTimeouts") procSetCommTimeouts = modkernel32.NewProc("SetCommTimeouts")
procSetConsoleCursorPosition = modkernel32.NewProc("SetConsoleCursorPosition") procSetConsoleCursorPosition = modkernel32.NewProc("SetConsoleCursorPosition")
procSetConsoleMode = modkernel32.NewProc("SetConsoleMode") procSetConsoleMode = modkernel32.NewProc("SetConsoleMode")
@ -342,7 +352,6 @@ var (
procSetDefaultDllDirectories = modkernel32.NewProc("SetDefaultDllDirectories") procSetDefaultDllDirectories = modkernel32.NewProc("SetDefaultDllDirectories")
procSetDllDirectoryW = modkernel32.NewProc("SetDllDirectoryW") procSetDllDirectoryW = modkernel32.NewProc("SetDllDirectoryW")
procSetEndOfFile = modkernel32.NewProc("SetEndOfFile") procSetEndOfFile = modkernel32.NewProc("SetEndOfFile")
procSetFileValidData = modkernel32.NewProc("SetFileValidData")
procSetEnvironmentVariableW = modkernel32.NewProc("SetEnvironmentVariableW") procSetEnvironmentVariableW = modkernel32.NewProc("SetEnvironmentVariableW")
procSetErrorMode = modkernel32.NewProc("SetErrorMode") procSetErrorMode = modkernel32.NewProc("SetErrorMode")
procSetEvent = modkernel32.NewProc("SetEvent") procSetEvent = modkernel32.NewProc("SetEvent")
@ -351,6 +360,7 @@ var (
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle") procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
procSetFilePointer = modkernel32.NewProc("SetFilePointer") procSetFilePointer = modkernel32.NewProc("SetFilePointer")
procSetFileTime = modkernel32.NewProc("SetFileTime") procSetFileTime = modkernel32.NewProc("SetFileTime")
procSetFileValidData = modkernel32.NewProc("SetFileValidData")
procSetHandleInformation = modkernel32.NewProc("SetHandleInformation") procSetHandleInformation = modkernel32.NewProc("SetHandleInformation")
procSetInformationJobObject = modkernel32.NewProc("SetInformationJobObject") procSetInformationJobObject = modkernel32.NewProc("SetInformationJobObject")
procSetNamedPipeHandleState = modkernel32.NewProc("SetNamedPipeHandleState") procSetNamedPipeHandleState = modkernel32.NewProc("SetNamedPipeHandleState")
@ -361,6 +371,7 @@ var (
procSetStdHandle = modkernel32.NewProc("SetStdHandle") procSetStdHandle = modkernel32.NewProc("SetStdHandle")
procSetVolumeLabelW = modkernel32.NewProc("SetVolumeLabelW") procSetVolumeLabelW = modkernel32.NewProc("SetVolumeLabelW")
procSetVolumeMountPointW = modkernel32.NewProc("SetVolumeMountPointW") procSetVolumeMountPointW = modkernel32.NewProc("SetVolumeMountPointW")
procSetupComm = modkernel32.NewProc("SetupComm")
procSizeofResource = modkernel32.NewProc("SizeofResource") procSizeofResource = modkernel32.NewProc("SizeofResource")
procSleepEx = modkernel32.NewProc("SleepEx") procSleepEx = modkernel32.NewProc("SleepEx")
procTerminateJobObject = modkernel32.NewProc("TerminateJobObject") procTerminateJobObject = modkernel32.NewProc("TerminateJobObject")
@ -379,6 +390,7 @@ var (
procVirtualQueryEx = modkernel32.NewProc("VirtualQueryEx") procVirtualQueryEx = modkernel32.NewProc("VirtualQueryEx")
procVirtualUnlock = modkernel32.NewProc("VirtualUnlock") procVirtualUnlock = modkernel32.NewProc("VirtualUnlock")
procWTSGetActiveConsoleSessionId = modkernel32.NewProc("WTSGetActiveConsoleSessionId") procWTSGetActiveConsoleSessionId = modkernel32.NewProc("WTSGetActiveConsoleSessionId")
procWaitCommEvent = modkernel32.NewProc("WaitCommEvent")
procWaitForMultipleObjects = modkernel32.NewProc("WaitForMultipleObjects") procWaitForMultipleObjects = modkernel32.NewProc("WaitForMultipleObjects")
procWaitForSingleObject = modkernel32.NewProc("WaitForSingleObject") procWaitForSingleObject = modkernel32.NewProc("WaitForSingleObject")
procWriteConsoleW = modkernel32.NewProc("WriteConsoleW") procWriteConsoleW = modkernel32.NewProc("WriteConsoleW")
@ -1641,6 +1653,22 @@ func CancelIoEx(s Handle, o *Overlapped) (err error) {
return return
} }
func ClearCommBreak(handle Handle) (err error) {
r1, _, e1 := syscall.Syscall(procClearCommBreak.Addr(), 1, uintptr(handle), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func ClearCommError(handle Handle, lpErrors *uint32, lpStat *ComStat) (err error) {
r1, _, e1 := syscall.Syscall(procClearCommError.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(lpErrors)), uintptr(unsafe.Pointer(lpStat)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func CloseHandle(handle Handle) (err error) { func CloseHandle(handle Handle) (err error) {
r1, _, e1 := syscall.Syscall(procCloseHandle.Addr(), 1, uintptr(handle), 0, 0) r1, _, e1 := syscall.Syscall(procCloseHandle.Addr(), 1, uintptr(handle), 0, 0)
if r1 == 0 { if r1 == 0 {
@ -1845,6 +1873,14 @@ func DeviceIoControl(handle Handle, ioControlCode uint32, inBuffer *byte, inBuff
return return
} }
func DisconnectNamedPipe(pipe Handle) (err error) {
r1, _, e1 := syscall.Syscall(procDisconnectNamedPipe.Addr(), 1, uintptr(pipe), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetProcessHandle Handle, lpTargetHandle *Handle, dwDesiredAccess uint32, bInheritHandle bool, dwOptions uint32) (err error) { func DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetProcessHandle Handle, lpTargetHandle *Handle, dwDesiredAccess uint32, bInheritHandle bool, dwOptions uint32) (err error) {
var _p0 uint32 var _p0 uint32
if bInheritHandle { if bInheritHandle {
@ -1857,6 +1893,14 @@ func DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetP
return return
} }
func EscapeCommFunction(handle Handle, dwFunc uint32) (err error) {
r1, _, e1 := syscall.Syscall(procEscapeCommFunction.Addr(), 2, uintptr(handle), uintptr(dwFunc), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func ExitProcess(exitcode uint32) { func ExitProcess(exitcode uint32) {
syscall.Syscall(procExitProcess.Addr(), 1, uintptr(exitcode), 0, 0) syscall.Syscall(procExitProcess.Addr(), 1, uintptr(exitcode), 0, 0)
return return
@ -2058,6 +2102,22 @@ func GetActiveProcessorCount(groupNumber uint16) (ret uint32) {
return return
} }
func GetCommModemStatus(handle Handle, lpModemStat *uint32) (err error) {
r1, _, e1 := syscall.Syscall(procGetCommModemStatus.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(lpModemStat)), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func GetCommState(handle Handle, lpDCB *DCB) (err error) {
r1, _, e1 := syscall.Syscall(procGetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(lpDCB)), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) { func GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) {
r1, _, e1 := syscall.Syscall(procGetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0) r1, _, e1 := syscall.Syscall(procGetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0)
if r1 == 0 { if r1 == 0 {
@ -2810,6 +2870,14 @@ func PulseEvent(event Handle) (err error) {
return return
} }
func PurgeComm(handle Handle, dwFlags uint32) (err error) {
r1, _, e1 := syscall.Syscall(procPurgeComm.Addr(), 2, uintptr(handle), uintptr(dwFlags), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func QueryDosDevice(deviceName *uint16, targetPath *uint16, max uint32) (n uint32, err error) { func QueryDosDevice(deviceName *uint16, targetPath *uint16, max uint32) (n uint32, err error) {
r0, _, e1 := syscall.Syscall(procQueryDosDeviceW.Addr(), 3, uintptr(unsafe.Pointer(deviceName)), uintptr(unsafe.Pointer(targetPath)), uintptr(max)) r0, _, e1 := syscall.Syscall(procQueryDosDeviceW.Addr(), 3, uintptr(unsafe.Pointer(deviceName)), uintptr(unsafe.Pointer(targetPath)), uintptr(max))
n = uint32(r0) n = uint32(r0)
@ -2924,6 +2992,30 @@ func ResumeThread(thread Handle) (ret uint32, err error) {
return return
} }
func SetCommBreak(handle Handle) (err error) {
r1, _, e1 := syscall.Syscall(procSetCommBreak.Addr(), 1, uintptr(handle), 0, 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func SetCommMask(handle Handle, dwEvtMask uint32) (err error) {
r1, _, e1 := syscall.Syscall(procSetCommMask.Addr(), 2, uintptr(handle), uintptr(dwEvtMask), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func SetCommState(handle Handle, lpDCB *DCB) (err error) {
r1, _, e1 := syscall.Syscall(procSetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(lpDCB)), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) { func SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) {
r1, _, e1 := syscall.Syscall(procSetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0) r1, _, e1 := syscall.Syscall(procSetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0)
if r1 == 0 { if r1 == 0 {
@ -2989,14 +3081,6 @@ func SetEndOfFile(handle Handle) (err error) {
return return
} }
func SetFileValidData(handle Handle, validDataLength int64) (err error) {
r1, _, e1 := syscall.Syscall(procSetFileValidData.Addr(), 2, uintptr(handle), uintptr(validDataLength), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func SetEnvironmentVariable(name *uint16, value *uint16) (err error) { func SetEnvironmentVariable(name *uint16, value *uint16) (err error) {
r1, _, e1 := syscall.Syscall(procSetEnvironmentVariableW.Addr(), 2, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(value)), 0) r1, _, e1 := syscall.Syscall(procSetEnvironmentVariableW.Addr(), 2, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(value)), 0)
if r1 == 0 { if r1 == 0 {
@ -3060,6 +3144,14 @@ func SetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetim
return return
} }
func SetFileValidData(handle Handle, validDataLength int64) (err error) {
r1, _, e1 := syscall.Syscall(procSetFileValidData.Addr(), 2, uintptr(handle), uintptr(validDataLength), 0)
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error) { func SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error) {
r1, _, e1 := syscall.Syscall(procSetHandleInformation.Addr(), 3, uintptr(handle), uintptr(mask), uintptr(flags)) r1, _, e1 := syscall.Syscall(procSetHandleInformation.Addr(), 3, uintptr(handle), uintptr(mask), uintptr(flags))
if r1 == 0 { if r1 == 0 {
@ -3145,6 +3237,14 @@ func SetVolumeMountPoint(volumeMountPoint *uint16, volumeName *uint16) (err erro
return return
} }
func SetupComm(handle Handle, dwInQueue uint32, dwOutQueue uint32) (err error) {
r1, _, e1 := syscall.Syscall(procSetupComm.Addr(), 3, uintptr(handle), uintptr(dwInQueue), uintptr(dwOutQueue))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func SizeofResource(module Handle, resInfo Handle) (size uint32, err error) { func SizeofResource(module Handle, resInfo Handle) (size uint32, err error) {
r0, _, e1 := syscall.Syscall(procSizeofResource.Addr(), 2, uintptr(module), uintptr(resInfo), 0) r0, _, e1 := syscall.Syscall(procSizeofResource.Addr(), 2, uintptr(module), uintptr(resInfo), 0)
size = uint32(r0) size = uint32(r0)
@ -3291,6 +3391,14 @@ func WTSGetActiveConsoleSessionId() (sessionID uint32) {
return return
} }
func WaitCommEvent(handle Handle, lpEvtMask *uint32, lpOverlapped *Overlapped) (err error) {
r1, _, e1 := syscall.Syscall(procWaitCommEvent.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(lpEvtMask)), uintptr(unsafe.Pointer(lpOverlapped)))
if r1 == 0 {
err = errnoErr(e1)
}
return
}
func waitForMultipleObjects(count uint32, handles uintptr, waitAll bool, waitMilliseconds uint32) (event uint32, err error) { func waitForMultipleObjects(count uint32, handles uintptr, waitAll bool, waitMilliseconds uint32) (event uint32, err error) {
var _p0 uint32 var _p0 uint32
if waitAll { if waitAll {

View file

@ -77,7 +77,7 @@ github.com/tkrajina/go-reflector/reflector
# github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 # github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
## explicit; go 1.16 ## explicit; go 1.16
github.com/tylertravisty/go-utils/random github.com/tylertravisty/go-utils/random
# github.com/tylertravisty/rumble-livestream-lib-go v0.3.5 # github.com/tylertravisty/rumble-livestream-lib-go v0.5.1
## explicit; go 1.19 ## explicit; go 1.19
github.com/tylertravisty/rumble-livestream-lib-go github.com/tylertravisty/rumble-livestream-lib-go
# github.com/valyala/bytebufferpool v1.0.0 # github.com/valyala/bytebufferpool v1.0.0
@ -140,14 +140,14 @@ github.com/wailsapp/wails/v2/pkg/options/linux
github.com/wailsapp/wails/v2/pkg/options/mac github.com/wailsapp/wails/v2/pkg/options/mac
github.com/wailsapp/wails/v2/pkg/options/windows github.com/wailsapp/wails/v2/pkg/options/windows
github.com/wailsapp/wails/v2/pkg/runtime github.com/wailsapp/wails/v2/pkg/runtime
# golang.org/x/crypto v0.21.0 # golang.org/x/crypto v0.22.0
## explicit; go 1.18 ## explicit; go 1.18
golang.org/x/crypto/acme golang.org/x/crypto/acme
golang.org/x/crypto/acme/autocert golang.org/x/crypto/acme/autocert
# golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 # golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
## explicit; go 1.20 ## explicit; go 1.20
golang.org/x/exp/constraints golang.org/x/exp/constraints
# golang.org/x/net v0.22.0 # golang.org/x/net v0.24.0
## explicit; go 1.18 ## explicit; go 1.18
golang.org/x/net/context golang.org/x/net/context
golang.org/x/net/html golang.org/x/net/html
@ -158,7 +158,7 @@ golang.org/x/net/http2/h2c
golang.org/x/net/http2/hpack golang.org/x/net/http2/hpack
golang.org/x/net/idna golang.org/x/net/idna
golang.org/x/net/websocket golang.org/x/net/websocket
# golang.org/x/sys v0.18.0 # golang.org/x/sys v0.19.0
## explicit; go 1.18 ## explicit; go 1.18
golang.org/x/sys/unix golang.org/x/sys/unix
golang.org/x/sys/windows golang.org/x/sys/windows