Browse Source

Moved to a separate repository; moved code to src dir

main
Unbewohnte 2 years ago
commit
67351eebc7
  1. 2
      .gitignore
  2. 9
      LICENSE
  3. 111
      README.txt
  4. 101
      src/config/config.go
  5. 36
      src/discordhooks/discordhook.go
  6. 5
      src/go.mod
  7. 4
      src/go.sum
  8. 110
      src/main.go
  9. 116
      src/twitchhooks/twitchhook.go
  10. 58
      src/vkhooks/vkhook.go

2
.gitignore vendored

@ -0,0 +1,2 @@
config.cfg
twitch-hooks

9
LICENSE

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2021 Unbewohne | Nikolay Kasyanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

111
README.txt

@ -0,0 +1,111 @@
_______ _ _ _ _ _
|__ __| (_) | | | | | | |
| |_ ___| |_ ___| |__ ______| |__ ___ ___ | | _____
| \ \ /\ / / | __/ __| '_ \______| '_ \ / _ \ / _ \| |/ / __|
| |\ V V /| | || (__| | | | | | | | (_) | (_) | <\__ \
|_| \_/\_/ |_|\__\___|_| |_| |_| |_|\___/ \___/|_|\_\___/
by Unbewohnte
[Eng]
! Purpose
This program`s purpose is to send custom messages
to Vk`s users/group chats and discord`s text channels
via webhooks when the Twitch stream is online.
! First run
When you run it the first time - it`ll generate a config.cfg
in your working directory and exit.
! Config file
Config file`s contents are present in JSON format.
Field "TwitchName" must contain Twitch channel`s name. The
CLI will be checking if this user has started streaming or not.
In block "Keys" you should paste at least all Twitch keys and
at least one of the fields that are left (Discord`s webhook url or Vk`s api key)
If "force-send" == true - the program won`t check for any streams and just
send messages you`ve stated it to send.
In block "Messages" you can specify what kind of messages the CLI will
send in case of the stream. Usually it`s a little opening followed by a
stream`s link.
!! "receiver_id" and "is_group_chat"
If you want to send a message to the group chat in VK and
set "is_group_chat" to true, then "receiver_id" must be your
personal id of that group chat. If you navigate to it in your
browser - you`ll see such pattern in the end of the URL: c20. In this case
20 is my personal id for some of my group chats. You should
set yours to "receiver_id".
! Next runs
After you`ve finished your configuration - the program`s ready to
work. Just execute it again and it`ll check every 5 minutes for specified
user`s stream. If it`s started - it`ll send your custom messages and exit.
! Api keys
!! Twitch
https://dev.twitch.tv/console - here, create an app and grab all the necessary keys
!! Discord
Create a webhook in the server`s settings and copy its URL
!! Vk
Create your own app, or proceed here: https://vkhost.github.io/ and grab your API key without a headache
[Ru]
! Назначение
Эта программа предназначена для отсылания оповещений о начавшемся
Twitch стриме на указанном канале пользователям/группе вКонтакте и
в текстовый канал Discord через webhook
! Первый запуск
При первом запуске cli генерирует файл в рабочей директории с именем
"config.cfg".
! Конфигурационный файл
Файл структурирован в виде JSON формата.
В поле "TwitchName" следует указать имя канала
на Твиче. Программа будет отслеживать начало стрима данного
пользователя.
В блоке "Keys" следует вставить как минимум все ключи от Twitch и
хотя-бы один из оставшихся url или ключей от ВК или Дискорда.
Если "force-send" == true - программа не станет ждать начала стрима
и сразу отошлёт указанные вами сообщения
В блоке "Messages" следует указать сообщение, которые вы хотите
отослать в случае начала стрима. Обычно это небольшое вступление
и ссылка на стрим.
!! "receiver_id" и "is_group_chat"
Если вы хотите отослать сообщение в групповой чат ВК и
выставили is_group_chat на true, то receiver_id будет ваше
личное ид данного чата. Чтобы его получить, зайдите в него и
в конце URL вы заметите что-то наподобие такого: с20. 20 в данном
случае и есть ид группового чата.
! Последующие запуски
После настройки конфигурационного файла программа готова к
полному использованию. Просто снова запустите её и она каждые
5 минут будет проверять наличие стрима на указанном канале.
Если стрим идёт - она отправит сообщения и закроется.
! Api ключи
!! Twitch
https://dev.twitch.tv/console - создай своё приложение и забери все необходимые ключи
!! Discord
Создай вебхук в настройках сервера и скопируй ссылку
!! Vk
Создай своё приложение, или просто зайди сюда: https://vkhost.github.io/ и забери свой ключ без головной боли

101
src/config/config.go

@ -0,0 +1,101 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"twitch-hooks/discordhooks"
"twitch-hooks/twitchhooks"
"twitch-hooks/vkhooks"
)
const DefaultConfigFilename string = "config.cfg"
type keys struct {
Twitch twitchhooks.Keys
Discord discordhooks.Webhook
VK vkhooks.ApiKey
}
type messages struct {
DiscordMessage discordhooks.Message
VKmessage vkhooks.Message
}
type Config struct {
TwitchName string
Keys keys
ForceSend bool `json:"force-send"`
Messages messages
}
// Checks if config file exists
func ConfigExists(configPath string) bool {
_, err := os.Stat(configPath)
if err != nil {
return false
}
return true
}
// Creates a new config file in specified directory
func CreateConfig(dir string) error {
// create a config file in the same directory
configF, err := os.Create(filepath.Join(dir, DefaultConfigFilename))
if err != nil {
return fmt.Errorf("could not create a config file: %s", err)
}
defer configF.Close()
// write default config fields
defaults, err := json.MarshalIndent(&Config{}, "", " ")
if err != nil {
return fmt.Errorf("could not marshal default config fields: %s", err)
}
_, err = configF.Write(defaults)
if err != nil {
return fmt.Errorf("could not write defaults to config: %s", err)
}
return nil
}
// Opens and reads config file, returns `Config` struct.
// If ReadConfig cannot unmarshal config file - it creates a new one with
// all default fields
func ReadConfig(pathToConfig string) (*Config, error) {
// get config`s contents
configContents, err := os.ReadFile(pathToConfig)
if err != nil {
return nil, fmt.Errorf("could not read config: %s", err)
}
var config Config
err = json.Unmarshal(configContents, &config)
if err != nil {
_ = CreateConfig(filepath.Dir(pathToConfig))
return nil, fmt.Errorf("could not unmarshal config: %s\nCreatead a new one", err)
}
// remove uneccessary spaces
config.Keys.Discord.WebhookUrl = strings.TrimSpace(config.Keys.Discord.WebhookUrl)
config.Keys.Twitch.ClientID = strings.TrimSpace(config.Keys.Twitch.ClientID)
config.Keys.Twitch.ClientSecret = strings.TrimSpace(config.Keys.Twitch.ClientSecret)
config.Keys.VK.Key = strings.TrimSpace(config.Keys.VK.Key)
// validate inputs
if config.Keys.Discord.WebhookUrl == "" &&
config.Keys.Twitch.ClientID == "" &&
config.Keys.Twitch.ClientSecret == "" &&
config.Keys.VK.Key == "" {
return nil, fmt.Errorf("does not use any keys")
}
if len(config.TwitchName) < 2 {
return nil, fmt.Errorf("twitch name is too short")
}
return &config, nil
}

36
src/discordhooks/discordhook.go

@ -0,0 +1,36 @@
package discordhooks
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type Webhook struct {
WebhookUrl string
}
const contentTypeJson string = "application/json"
type Message struct {
Message string `json:"content"`
Username string `json:"username"`
AvatarURL string `json:"avatar_url"`
}
// Post Message struct to given webhook
func Post(webhookUrl string, message Message) error {
json, err := json.Marshal(&message)
if err != nil {
return fmt.Errorf("could not marshal given JsonMessage: %s", err)
}
resp, err := http.Post(webhookUrl, contentTypeJson, bytes.NewBuffer(json))
if err != nil {
return fmt.Errorf("could not POST to given url: %s", err)
}
defer resp.Body.Close()
return nil
}

5
src/go.mod

@ -0,0 +1,5 @@
module twitch-hooks
go 1.16
require github.com/go-vk-api/vk v0.0.0-20200129183856-014d9b8adc96

4
src/go.sum

@ -0,0 +1,4 @@
github.com/go-vk-api/vk v0.0.0-20200129183856-014d9b8adc96 h1:gFOrXsbjC+XvHIVMx608V/mRVZwmU9xofR1Vht9p3Zg=
github.com/go-vk-api/vk v0.0.0-20200129183856-014d9b8adc96/go.mod h1:UeBKPsuqp+KSBDtC7gr+Lng+TEaoFXT/mpOs6Tj7tFQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

110
src/main.go

@ -0,0 +1,110 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"time"
"twitch-hooks/config"
"twitch-hooks/discordhooks"
"twitch-hooks/twitchhooks"
"twitch-hooks/vkhooks"
)
var (
logo string = ` _______ _ _ _ _ _
|__ __| (_) | | | | | | |
| |_ ___| |_ ___| |__ ______| |__ ___ ___ | | _____
| \ \ /\ / / | __/ __| '_ \______| '_ \ / _ \ / _ \| |/ / __|
| |\ V V /| | || (__| | | | | | | | (_) | (_) | <\__ \
|_| \_/\_/ |_|\__\___|_| |_| |_| |_|\___/ \___/|_|\_\___/ by Unbewohnte`
Config config.Config
pathToConfig *string = flag.String("config", config.DefaultConfigFilename, "Specifies path to a config in another directory")
delay *uint = flag.Uint("delay", 5000, "Delay in seconds for each check cycle")
)
func init() {
// process the config file
flag.Parse()
if *pathToConfig != config.DefaultConfigFilename {
// config in another place
config.ReadConfig(*pathToConfig)
}
if !config.ConfigExists(*pathToConfig) {
// there is no existing config file;
// create a new one and exit
err := config.CreateConfig(filepath.Dir(*pathToConfig))
if err != nil {
panic(err)
}
fmt.Println("Created a new config file")
os.Exit(0)
}
configContents, err := config.ReadConfig(*pathToConfig)
if err != nil {
panic(err)
}
Config = *configContents
}
func main() {
fmt.Println(logo)
if Config.Keys.Twitch.ClientID == "" || Config.Keys.Twitch.ClientSecret == "" {
// no twitch api key used. Notify the user and check for the force-send flag
fmt.Println("No Twitch API keys found")
if !Config.ForceSend {
// not forced to send messages. Exiting
fmt.Println("Not forced to send. Exiting...")
os.Exit(0)
}
}
for {
// retrieve access token
tokenResp, err := twitchhooks.GetToken(&Config.Keys.Twitch)
if err != nil {
panic(err)
}
// check if live
is_live, err := twitchhooks.IsLive(Config.TwitchName, &twitchhooks.RequestOptions{
ApplicationKeys: Config.Keys.Twitch,
AccessToken: *tokenResp,
})
if err != nil {
panic(err)
}
if is_live || Config.ForceSend {
// live or forced to send -> send alerts
if Config.Keys.Discord.WebhookUrl != "" {
err := discordhooks.Post(Config.Keys.Discord.WebhookUrl, Config.Messages.DiscordMessage)
if err != nil {
panic(err)
}
}
if Config.Keys.VK.Key != "" {
vkhooks.Initialise(Config.Keys.VK.Key)
err := vkhooks.Send(Config.Messages.VKmessage)
if err != nil {
panic(err)
}
}
// alerted. Now exiting
fmt.Println("Alerts has been sent ! My work is done here...")
os.Exit(0)
}
duration, _ := time.ParseDuration(fmt.Sprintf("%ds", *delay))
time.Sleep(duration)
}
}

116
src/twitchhooks/twitchhook.go

@ -0,0 +1,116 @@
package twitchhooks
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
type Keys struct {
ClientID string
ClientSecret string
}
// access token response struct
type TokenResponse struct {
AcessToken string `json:"access_token"`
ExpiresIn uint `json:"expires_in"`
TokenType string `json:"token_type"`
}
type RequestOptions struct {
ApplicationKeys Keys
AccessToken TokenResponse
}
// Retrieves access token from Twitch
func GetToken(keys *Keys) (*TokenResponse, error) {
getTokenUrl := fmt.Sprintf("https://id.twitch.tv/oauth2/token?client_id=%s&client_secret=%s&grant_type=client_credentials",
keys.ClientID, keys.ClientSecret)
resp, err := http.Post(getTokenUrl, "", bytes.NewBuffer([]byte{}))
if err != nil {
return nil, fmt.Errorf("could not make a post request: %s", err)
}
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var tokenResp TokenResponse
err = json.Unmarshal(content, &tokenResp)
if err != nil {
return nil, fmt.Errorf("could not unmarshal token response: %s", err)
}
return &tokenResp, nil
}
// gets data about user from api endpoint
func GetUser(displayname string, options *RequestOptions) (string, error) {
requestUrl := fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", displayname)
httpClient := http.Client{}
request, err := http.NewRequest("GET", requestUrl, new(bytes.Buffer))
if err != nil {
return "", fmt.Errorf("could not create a new twitch request: %s", err)
}
defer request.Body.Close()
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken.AcessToken))
request.Header.Add("Client-id", options.ApplicationKeys.ClientID)
response, err := httpClient.Do(request)
if err != nil {
return "", fmt.Errorf("could not make a request to twitch api: %s", err)
}
data, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
return string(data), nil
}
// Checks if the user streaming right now
func IsLive(displayname string, options *RequestOptions) (bool, error) {
requestUrl := fmt.Sprintf("https://api.twitch.tv/helix/streams?user_login=%s", displayname)
httpClient := http.Client{}
request, err := http.NewRequest("GET", requestUrl, new(bytes.Buffer))
if err != nil {
return false, fmt.Errorf("could not create a new twitch request: %s", err)
}
defer request.Body.Close()
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", options.AccessToken.AcessToken))
request.Header.Add("Client-id", options.ApplicationKeys.ClientID)
response, err := httpClient.Do(request)
if err != nil {
return false, fmt.Errorf("could not make a request to twitch api: %s", err)
}
data, err := io.ReadAll(response.Body)
if err != nil {
return false, err
}
// check if got an empty response -> offline
if len(data) <= 28 {
return false, nil
}
return true, nil
}
func GetStream() {
}

58
src/vkhooks/vkhook.go

@ -0,0 +1,58 @@
package vkhooks
import (
"fmt"
vk "github.com/go-vk-api/vk"
)
type ApiKey struct {
Key string
}
var client *vk.Client
type Message struct {
Message string `json:"message"`
GroupChat bool `json:"is_group_chat"`
ID uint `json:"receiver_id"`
}
func Initialise(vkapikey string) {
// create a client on init
vkClient, err := vk.NewClientWithOptions(
vk.WithToken(vkapikey),
)
if err != nil {
panic(err)
}
client = vkClient
}
// Sends message to the given id
func Send(message Message) error {
switch message.GroupChat {
case true:
err := client.CallMethod("messages.send", vk.RequestParams{
"chat_id": message.ID,
"message": message.Message,
"random_id": 0,
}, nil)
if err != nil {
return fmt.Errorf("could not send vk message: %s", err)
}
case false:
err := client.CallMethod("messages.send", vk.RequestParams{
"peer_id": message.ID,
"message": message.Message,
"random_id": 0,
}, nil)
if err != nil {
return fmt.Errorf("could not send vk message: %s", err)
}
}
return nil
}
Loading…
Cancel
Save