0
0
Fork 0
tts-stuff/main.go

987 lines
25 KiB
Go

package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
type ConfigStruct struct {
BotToken string `json:"botToken"`
BotAppId string `json:"botAppId"`
BotAllowedHosts map[string]bool `json:"botAllowedHosts"`
BotTransferShUrl string `json:"botTransferShUrl"`
BotWebhookUrls map[string]string `json:"botWebhookUrls"`
ServerPort int `json:"serverPort"`
ApiRootUrl string `json:"apiRootUrl"`
ApiAdminPrefix string `json:"apiAdminPrefix"`
TextFile string `json:"textFile"`
AudioFile string `json:"audioFile"`
WebUiFile string `json:"webUiFile"`
ApiLogFile string `json:"apiLogFile"`
DiscordLogFile string `json:"discordLogFile"`
BannedIpsFile string `json:"bannedIpsFile"`
BannedDiscordUsersFile string `json:"bannedDiscordUsersFile"`
Voices [][]string `json:"voices"`
TtsCommands map[string][][]string `json:"ttsCommands"`
GetDurationCommand []string `json:"getDurationCommand"`
KillCommand []string `json:"killCommand"`
Timeout int `json:"timeout"`
TimeoutTickerInterval int `json:"timeoutTickerInterval"`
AllowedCharacters string `json:"allowedCharacters"`
TextLengthLimit int `json:"textLengthLimit"`
RateLimit []int `json:"rateLimit"`
}
type ApiLogStruct struct {
DateTimeString string `json:"dateTimeString"`
Timestamp int64 `json:"timestamp"`
UserAgent string `json:"userAgent"`
Ip string `json:"ip"`
TextLength int `json:"textLength"`
Voice string `json:"voice"`
}
type DiscordLogStruct struct {
DateTimeString string `json:"dateTimeString"`
Timestamp int64 `json:"timestamp"`
GuildId string `json:"guildId"`
ChannelId string `json:"channelId"`
UserId string `json:"userId"`
Command string `json:"command"`
TextHost string `json:"textHost"`
TextLength int `json:"textLength"`
Voice string `json:"voice"`
}
type ServerInfoStruct struct {
Busy bool `json:"busy"`
Requests []int `json:"requests"`
}
var (
configFile = flag.String("configfile", "config.json", "Config file")
enableBot = flag.Bool("enablebot", false, "Enable bot")
allInterfaces = flag.Bool("allinterfaces", false, "Listen on all interfaces instead of localhost only")
allowedCharacters = map[rune]bool{}
bannedIps = map[string]bool{}
bannedDiscordUsers = map[string]bool{}
c = make(chan int)
)
var config ConfigStruct
var dg *discordgo.Session
var botName string
var botAvatar string
var apiLogFile *os.File
var discordLogFile *os.File
func GetIp(r *http.Request) string {
address := r.Header.Get("X-Real-Ip")
if address == "" {
address = r.Header.Get("X-Forwarded-For")
}
if address == "" {
address = r.RemoteAddr
}
ip, _, err := net.SplitHostPort(address)
if err != nil {
return address
}
return ip
}
func HttpHandler(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, config.ApiAdminPrefix) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if r.URL.Path == config.ApiAdminPrefix {
http.Error(w, "404", http.StatusNotFound)
} else if r.URL.Path == fmt.Sprintf("%s/text", config.ApiAdminPrefix) {
textFile, err := os.Open(config.TextFile)
if err != nil {
fmt.Println(err)
http.Error(w, "500", http.StatusInternalServerError)
return
}
io.Copy(w, textFile)
textFile.Close()
} else if r.URL.Path == fmt.Sprintf("%s/updatebotcommands", config.ApiAdminPrefix) {
if *enableBot {
q := r.URL.Query()
dg2, err := discordgo.New("Bot " + config.BotToken)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var voiceChoices []*discordgo.ApplicationCommandOptionChoice
if q.Has("ttsd") || q.Has("ttst") {
for _, v := range config.Voices {
voiceChoices = append(voiceChoices, &discordgo.ApplicationCommandOptionChoice{
Name: v[1],
Value: v[0],
})
}
}
if q.Has("ttsd") && q.Has("ttst") {
_, err = dg2.ApplicationCommandBulkOverwrite(config.BotAppId, q.Get("s"), []*discordgo.ApplicationCommand{
{
Name: "ttsd",
Description: "No description",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "text",
Description: "Text link or JSON string",
Type: discordgo.ApplicationCommandOptionString,
MaxLength: config.TextLengthLimit,
Required: true,
},
{
Name: "voice",
Description: "Voice",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
Choices: voiceChoices,
},
},
},
{
Name: "ttst",
Description: "No description",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "text",
Description: "Text link or JSON string",
Type: discordgo.ApplicationCommandOptionString,
MaxLength: config.TextLengthLimit,
Required: true,
},
{
Name: "voice",
Description: "Voice",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
Choices: voiceChoices,
},
},
},
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else if q.Has("ttsd") {
_, err = dg2.ApplicationCommandBulkOverwrite(config.BotAppId, q.Get("s"), []*discordgo.ApplicationCommand{
{
Name: "ttsd",
Description: "No description",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "text",
Description: "Text link or JSON string",
Type: discordgo.ApplicationCommandOptionString,
MaxLength: config.TextLengthLimit,
Required: true,
},
{
Name: "voice",
Description: "Voice",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
Choices: voiceChoices,
},
},
},
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else if q.Has("ttst") {
_, err = dg2.ApplicationCommandBulkOverwrite(config.BotAppId, q.Get("s"), []*discordgo.ApplicationCommand{
{
Name: "ttst",
Description: "No description",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "text",
Description: "Text link or JSON string",
Type: discordgo.ApplicationCommandOptionString,
MaxLength: config.TextLengthLimit,
Required: true,
},
{
Name: "voice",
Description: "Voice",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
Choices: voiceChoices,
},
},
},
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
_, err = dg2.ApplicationCommandBulkOverwrite(config.BotAppId, q.Get("s"), []*discordgo.ApplicationCommand{})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
} else {
http.Error(w, "404", http.StatusNotFound)
}
} else if r.URL.Path == fmt.Sprintf("%s/info", config.ApiAdminPrefix) {
c <- 2
} else if r.URL.Path == fmt.Sprintf("%s/reload", config.ApiAdminPrefix) {
c <- 3
} else {
c <- 4
}
return
}
ip := GetIp(r)
if bannedIps[ip] {
http.Error(w, "403", http.StatusForbidden)
return
}
if r.URL.Path == "/" {
if r.Method == http.MethodGet {
http.ServeFile(w, r, config.WebUiFile)
} else if r.Method == http.MethodPost {
decoder := json.NewDecoder(r.Body)
var strs []string
err := decoder.Decode(&strs)
if err != nil {
return
}
if len(strs) != 2 || len(strings.TrimSpace(strs[1])) == 0 || len(strs[1]) > config.TextLengthLimit {
http.Error(w, "400", http.StatusBadRequest)
return
}
for _, char := range strs[1] {
if !allowedCharacters[char] {
http.Error(w, "400", http.StatusBadRequest)
return
}
}
if ttsCommands, found := config.TtsCommands[strs[0]]; found {
c <- 0
switch <-c {
case 429:
http.Error(w, "429", http.StatusTooManyRequests)
return
case 503:
http.Error(w, "503", http.StatusServiceUnavailable)
return
}
defer func() {
os.Remove(config.TextFile)
os.Remove(config.AudioFile)
c <- 1
}()
textFile, err := os.Create(config.TextFile)
if err != nil {
fmt.Println(err)
http.Error(w, "500", http.StatusInternalServerError)
return
}
_, err = textFile.WriteString(strs[1])
if err != nil {
fmt.Println(err)
textFile.Close()
http.Error(w, "500", http.StatusInternalServerError)
return
}
err = textFile.Close()
if err != nil {
fmt.Println(err)
http.Error(w, "500", http.StatusInternalServerError)
return
}
for _, c := range ttsCommands {
cmd := exec.Command(c[0], c[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Println(err)
http.Error(w, "500", http.StatusInternalServerError)
return
}
}
getDurationOut, err := exec.Command(config.GetDurationCommand[0], config.GetDurationCommand[1:]...).Output()
if err != nil {
fmt.Println(err)
http.Error(w, "500", http.StatusInternalServerError)
return
}
durationString := strings.TrimSpace(string(getDurationOut))
if durationString == "" {
http.Error(w, "500", http.StatusInternalServerError)
return
}
durationFloat, err := strconv.ParseFloat(durationString, 64)
if err != nil {
http.Error(w, "500", http.StatusInternalServerError)
return
}
d := time.Duration(durationFloat * float64(time.Second))
if d < 100*time.Millisecond {
http.Error(w, "400", http.StatusBadRequest)
return
}
audioFile, err := os.Open(config.AudioFile)
if err != nil {
fmt.Println(err)
http.Error(w, "500", http.StatusInternalServerError)
return
}
w.Header().Set("Duration", d.String())
switch filepath.Ext(config.AudioFile) {
case ".wav":
w.Header().Set("Content-Type", "audio/wav")
case ".flac":
w.Header().Set("Content-Type", "audio/flac")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
io.Copy(w, audioFile)
audioFile.Close()
if apiLogFile != nil {
t := time.Now()
jsonBytes, err := json.Marshal(ApiLogStruct{
DateTimeString: t.Format(time.RFC3339),
Timestamp: t.Unix(),
UserAgent: r.Header.Get("User-Agent"),
Ip: ip,
TextLength: len(strs[1]),
Voice: strs[0],
})
if err == nil {
fmt.Fprintln(apiLogFile, string(jsonBytes))
} else {
fmt.Println(err)
}
}
} else {
http.Error(w, "400", http.StatusBadRequest)
}
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
} else {
http.Error(w, "404", http.StatusNotFound)
}
}
func main() {
flag.Parse()
configBytes, err := os.ReadFile(*configFile)
if err != nil {
panic(err)
}
err = json.Unmarshal(configBytes, &config)
if err != nil {
panic(err)
}
if config.ServerPort > 0 {
bannedIpsBytes, err := os.ReadFile(config.BannedIpsFile)
if err == nil {
json.Unmarshal(bannedIpsBytes, &bannedIps)
}
for _, ch := range config.AllowedCharacters {
allowedCharacters[ch] = true
}
if config.ApiLogFile != "" {
apiLogFile, err = os.OpenFile(config.ApiLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
defer apiLogFile.Close()
}
}
if *enableBot {
if config.DiscordLogFile != "" {
discordLogFile, err = os.OpenFile(config.DiscordLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
panic(err)
}
defer discordLogFile.Close()
}
bannedDiscordUsersBytes, err := os.ReadFile(config.BannedDiscordUsersFile)
if err == nil {
json.Unmarshal(bannedDiscordUsersBytes, &bannedDiscordUsersBytes)
}
dg, err := discordgo.New("Bot " + config.BotToken)
if err != nil {
panic(err)
}
dg.AddHandler(func(
s *discordgo.Session,
r *discordgo.Ready,
) {
botName = r.User.Username
botAvatar = r.User.Avatar
})
dg.AddHandler(func(
s *discordgo.Session,
i *discordgo.InteractionCreate,
) {
if bannedDiscordUsers[i.Interaction.Member.User.ID] {
return
}
data := i.ApplicationCommandData()
if data.Name == "ttsd" && config.BotWebhookUrls[i.Interaction.ChannelID] == "" {
return
}
values := map[string]string{}
for _, o := range data.Options {
values[o.Name] = strings.TrimSpace(o.StringValue())
}
text := ""
textHost := ""
if strings.HasPrefix(values["text"], "\"") {
err := json.Unmarshal([]byte(values["text"]), &text)
if err != nil {
dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error",
},
})
return
}
if data.Name == "ttsd" {
err = dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
} else {
err = dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
if err != nil {
return
}
} else {
_, err := url.ParseRequestURI(values["text"])
if err != nil {
dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error",
},
})
return
}
textUrl, err := url.Parse(values["text"])
if err != nil {
dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error",
},
})
return
}
if !config.BotAllowedHosts[textUrl.Host] {
dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("`%s` is not in allowed host list", textUrl.Host),
},
})
return
}
if textUrl.Host == "pastebin.com" && !strings.HasPrefix(textUrl.Path, "/raw/") {
textUrl.Path = fmt.Sprintf("/raw%s", textUrl.Path)
}
if textUrl.Host == "bin.sohamsen.me" && textUrl.Fragment != "" {
textUrl.RawQuery = "k=" + textUrl.EscapedFragment()
textUrl.Fragment = ""
}
if data.Name == "ttsd" {
err = dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
} else {
err = dg.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
if err != nil {
return
}
textResponse, err := http.Get(textUrl.String())
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
defer textResponse.Body.Close()
if textResponse.StatusCode != http.StatusOK {
content := fmt.Sprintf("`%s` returned error `%s`", textUrl.Host, textResponse.Status)
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
if !strings.HasPrefix(textResponse.Header.Get("Content-Type"), "text/plain") {
content := fmt.Sprintf("`%s` did not return plain text", textUrl.Host)
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
textResponseBodyBytes, err := io.ReadAll(textResponse.Body)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
text = string(textResponseBodyBytes)
textHost = textUrl.Host
}
apiJsonBytes, err := json.Marshal([]string{values["voice"], text})
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
apiResponse, err := http.Post(config.ApiRootUrl, "application/json", bytes.NewBuffer(apiJsonBytes))
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
defer apiResponse.Body.Close()
if apiResponse.StatusCode != http.StatusOK {
content := fmt.Sprintf("TTS API returned error `%s`", apiResponse.Status)
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
audioFile, err := io.ReadAll(apiResponse.Body)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
uploadRequestBody := &bytes.Buffer{}
writer := multipart.NewWriter(uploadRequestBody)
if data.Name == "ttsd" {
jsonPart, err := writer.CreateFormField("payload_json")
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
jsonPart.Write([]byte(fmt.Sprintf("{\"username\":\"%s\",\"avatar_url\":\"https://cdn.discordapp.com/avatars/%s/%s.png\"}", botName, config.BotAppId, botAvatar)))
}
var filename string
switch apiResponse.Header.Get("Content-Type") {
case "audio/wav":
filename = "file.wav"
case "audio/flac":
filename = "file.flac"
default:
filename = "file"
}
audioFilePart, err := writer.CreateFormFile("file", filename)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
_, err = audioFilePart.Write(audioFile)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
writer.Close()
var uploadUrl string
if data.Name == "ttsd" {
uploadUrl = config.BotWebhookUrls[i.Interaction.ChannelID]
} else {
uploadUrl = config.BotTransferShUrl
}
uploadRequest, err := http.NewRequest("POST", uploadUrl, uploadRequestBody)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
uploadRequest.Header.Add("Content-Type", writer.FormDataContentType())
if data.Name == "ttst" {
uploadRequest.Header.Add("Max-Days", "1")
}
client := &http.Client{}
uploadResponse, err := client.Do(uploadRequest)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
defer uploadResponse.Body.Close()
if uploadResponse.StatusCode != http.StatusOK {
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
uploadResponseBodyBytes, err := io.ReadAll(uploadResponse.Body)
if err != nil {
fmt.Println(err)
content := "Error"
dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
return
}
if data.Name == "ttsd" {
content := fmt.Sprintf("Duration: %s", apiResponse.Header.Get("Duration"))
_, err = dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
} else {
content := fmt.Sprintf("Duration: %s\nLink: <%s>", apiResponse.Header.Get("Duration"), strings.TrimSpace(string(uploadResponseBodyBytes)))
_, err = dg.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &content,
})
}
if err == nil && discordLogFile != nil {
t := time.Now()
jsonBytes, err := json.Marshal(DiscordLogStruct{
DateTimeString: t.Format(time.RFC3339),
Timestamp: t.Unix(),
GuildId: i.Interaction.GuildID,
ChannelId: i.Interaction.ChannelID,
UserId: i.Interaction.Member.User.ID,
Command: data.Name,
TextHost: textHost,
TextLength: len(text),
Voice: values["voice"],
})
if err == nil {
fmt.Fprintln(discordLogFile, string(jsonBytes))
} else {
fmt.Println(err)
}
}
})
err = dg.Open()
if err != nil {
panic(err)
}
defer dg.Close()
}
if config.ServerPort > 0 {
go func() {
http.HandleFunc("/", HttpHandler)
if *allInterfaces {
http.ListenAndServe(fmt.Sprintf(":%d", config.ServerPort), nil)
} else {
http.ListenAndServe(fmt.Sprintf("localhost:%d", config.ServerPort), nil)
}
}()
timeoutTicker := time.NewTicker(time.Duration(config.TimeoutTickerInterval) * time.Millisecond)
rateLimitTicker := time.NewTicker(time.Duration(config.RateLimit[1]) * time.Millisecond)
done := make(chan bool)
go func() {
requests := config.RateLimit[0]
startTime := time.Time{}
for {
select {
case <-done:
return
case i := <-c:
if i == 0 {
if startTime.IsZero() {
if requests == 0 {
c <- 429
} else {
startTime = time.Now()
requests--
c <- 0
}
} else {
c <- 503
}
} else if i == 1 {
startTime = time.Time{}
} else if i == 2 {
jsonBytes, err := json.Marshal(ServerInfoStruct{
Busy: !startTime.IsZero(),
Requests: []int{requests, config.RateLimit[0], config.RateLimit[1]},
})
if err == nil {
fmt.Println(string(jsonBytes))
}
} else if i == 3 {
configBytes, err := os.ReadFile(*configFile)
if err != nil {
panic(err)
}
for k := range config.BotWebhookUrls {
delete(config.BotWebhookUrls, k)
}
for k := range config.BotAllowedHosts {
delete(config.BotAllowedHosts, k)
}
for k := range config.TtsCommands {
delete(config.TtsCommands, k)
}
err = json.Unmarshal(configBytes, &config)
if err != nil {
panic(err)
}
for k := range bannedIps {
delete(bannedIps, k)
}
bannedIpsBytes, err := os.ReadFile(config.BannedIpsFile)
if err == nil {
json.Unmarshal(bannedIpsBytes, &bannedIps)
}
if *enableBot {
for k := range bannedDiscordUsers {
delete(bannedDiscordUsers, k)
}
bannedDiscordUsersBytes, err := os.ReadFile(config.BannedDiscordUsersFile)
if err == nil {
json.Unmarshal(bannedDiscordUsersBytes, &bannedDiscordUsers)
}
}
} else if i == 4 {
requests = config.RateLimit[0]
}
case t := <-timeoutTicker.C:
if !startTime.IsZero() && t.After(startTime.Add(time.Duration(config.Timeout)*time.Millisecond)) {
cmd := exec.Command(config.KillCommand[0], config.KillCommand[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Println(err)
}
}
case <-rateLimitTicker.C:
requests = config.RateLimit[0]
}
}
}()
fmt.Scanln()
timeoutTicker.Stop()
rateLimitTicker.Stop()
done <- true
} else {
fmt.Scanln()
}
}