Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
438 lines (400 sloc) 19.2 KB
package commands
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/ackersonde/bender-slackbot/structures"
"github.com/ackersonde/digitaloceans/common"
jsoniter "github.com/json-iterator/go"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
)
var api *slack.Client
var joinAPIKey = os.Getenv("CTX_JOIN_API_KEY")
var vpnGateway = os.Getenv("CTX_VPNC_GATEWAY")
var githubRunID = os.Getenv("GITHUB_RUN_ID")
// Logger to give senseful settings
var Logger = log.New(os.Stdout, "", log.LstdFlags)
// VPNCountry as default connection
var VPNCountry = "NL"
// SlackReportChannel default reporting channel for bot crons
var SlackReportChannel = os.Getenv("CTX_SLACK_CHANNEL")
// SetAPI sets singleton
func SetAPI(apiPassed *slack.Client) {
api = apiPassed
}
// CheckCommand is now commented
func CheckCommand(event *slackevents.MessageEvent, command string) {
args := strings.Fields(command)
params := slack.MsgOptionAsUser(true)
if args[0] == "scpxl" {
if len(args) > 1 {
// strip '<>' off url
downloadURL := strings.Trim(args[1], "<>")
uri, err := url.ParseRequestURI(downloadURL)
Logger.Printf("parsed %s from %s", uri.RequestURI(), downloadURL)
if err != nil {
api.PostMessage(event.Channel,
slack.MsgOptionText(
"Invalid URL for downloading! ("+err.Error()+
")", true), params)
} else {
remoteClient := scpRemoteConnectionConfiguration(structures.AndroidRCC)
if scpFileBetweenHosts(
remoteClient,
downloadURL,
structures.AndroidRCC.HostPath) {
api.PostMessage(event.Channel,
slack.MsgOptionText(
"Requested URL...", true), params)
} else {
api.PostMessage(event.Channel,
slack.MsgOptionText(
"Unable to download URL...", true), params)
}
}
} else {
api.PostMessage(event.Channel,
slack.MsgOptionText("Please provide source file URL!", true), params)
}
} else if args[0] == "crypto" {
response := checkEthereumValue() + "\n" + checkStellarLumensValue()
api.PostMessage(event.Channel,
slack.MsgOptionText(response, false), params)
} else if args[0] == "pgp" {
api.PostMessage(event.Channel,
slack.MsgOptionText(pgpKeys(), false), params)
} else if args[0] == "pi" {
api.PostMessage(event.Channel,
slack.MsgOptionText(raspberryPIChecks(), false), params)
} else if args[0] == "yt" {
if len(args) > 1 {
// strip '<>' off url
downloadURL := strings.Trim(args[1], "<>")
uri, err := url.ParseRequestURI(downloadURL)
Logger.Printf("parsed %s from %s", uri.RequestURI(), downloadURL)
if err != nil {
api.PostMessage(event.Channel,
slack.MsgOptionText(
"Invalid URL for downloading! ("+err.Error()+
")", true), params)
} else {
if downloadYoutubeVideo(uri.String()) {
api.PostMessage(event.Channel,
slack.MsgOptionText(
"Requested YouTube video...", true), params)
} else {
api.PostMessage(event.Channel,
slack.MsgOptionText(
"Unable to download YouTube video...", true), params)
}
}
} else {
api.PostMessage(event.Channel,
slack.MsgOptionText("Please provide YouTube video URL!", true), params)
}
} else if args[0] == "bb" {
result := ""
dateString := ""
if len(args) > 1 {
// TODO: use https://github.com/olebedev/when for Natural Language processing
gameDate, err := time.Parse("2006-01-02", args[1])
dateString = gameDate.Format("2006/month_01/day_02")
if err != nil {
result = "Couldn't figure out date '" + args[1] + "'. Try `help`"
api.PostMessage(event.Channel, slack.MsgOptionText(result, false), params)
return
}
}
result = ShowBBGames(dateString)
api.PostMessage(event.Channel, slack.MsgOptionText(result, false), params)
} else if args[0] == "do" {
response := ListDODroplets()
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "dd" {
if len(args) > 1 {
number, err := strconv.Atoi(args[1])
if err != nil {
api.PostMessage(event.Channel, slack.MsgOptionText("Invalid integer value for ID!", true), params)
} else {
result := common.DeleteDODroplet(number)
api.PostMessage(event.Channel, slack.MsgOptionText(result, true), params)
}
} else {
api.PostMessage(event.Channel, slack.MsgOptionText("Please provide Droplet ID from `do` cmd!", true), params)
}
} else if args[0] == "fsck" {
response := ""
if len(args) > 1 {
path := strings.Join(args[1:], " ")
response += CheckMediaDiskSpace(path)
response += CheckServerDiskSpace(path)
} else {
response += CheckMediaDiskSpace("")
response += CheckServerDiskSpace("")
}
api.PostMessage(event.Channel, slack.MsgOptionText(response, true), params)
} else if args[0] == "wgs" {
api.PostMessage(event.Channel, slack.MsgOptionText(wireguardShow(), true), params)
} else if args[0] == "wgu" {
api.PostMessage(event.Channel, slack.MsgOptionText(wireguardAction("up"), true), params)
} else if args[0] == "wgd" {
api.PostMessage(event.Channel, slack.MsgOptionText(wireguardAction("down"), true), params)
} else if args[0] == "wfs" {
api.PostMessage(event.Channel, slack.MsgOptionText(WifiAction("STATE"), true), params)
} else if args[0] == "wfu" {
api.PostMessage(event.Channel, slack.MsgOptionText(WifiAction("1"), true), params)
} else if args[0] == "wfu5" {
api.PostMessage(event.Channel, slack.MsgOptionText(WifiAction("5"), true), params)
} else if args[0] == "wfd" {
api.PostMessage(event.Channel, slack.MsgOptionText(WifiAction("0"), true), params)
} else if args[0] == "mv" {
if len(args) == 3 &&
(strings.HasPrefix(args[2], "movies") ||
strings.HasPrefix(args[2], "tv")) {
sourceFile := scrubParamOfHTTPMagicCrap(args[1])
destinationDir := args[2]
if strings.Contains(destinationDir, "..") || strings.HasPrefix(destinationDir, "/") {
msg := fmt.Sprintln("Please prefix destination w/ either `[movies|tv]`")
api.PostMessage(event.Channel, slack.MsgOptionText(msg, true), params)
} else if strings.Contains(sourceFile, "..") || strings.HasPrefix(sourceFile, "/") {
msg := fmt.Sprintf("Please specify file to move relative to `%s/torrents/`\n", piPlexPath)
api.PostMessage(event.Channel, slack.MsgOptionText(msg, true), params)
} else {
MoveTorrentFile(sourceFile, destinationDir)
}
} else {
msg := "Please provide a src file and destination [e.g. `movies` or `tv`]"
api.PostMessage(event.Channel, slack.MsgOptionText(msg, true), params)
}
} else if args[0] == "torq" {
var response string
if len(args) > 1 {
searchString := strings.Join(args[1:], " ")
searchStringURL := "/api?url=/q.php?q=" + url.QueryEscape(searchString)
response = parseTorrents(searchProxy(searchStringURL))
} else {
response = parseTop100(searchProxy("/api?url=/precompiled/data_top100_207.json"))
}
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "vpns" {
if len(args) > 1 {
VPNCountry = strings.ToUpper(args[1])
}
response := VpnPiTunnelChecks(VPNCountry)
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "vpnc" {
response := "Please provide a new VPN server (hint: output from `vpns`)"
if len(args) > 1 {
vpnServerDomain := strings.ToLower(scrubParamOfHTTPMagicCrap(args[1]))
// ensure vpnServerDomain has format e.g. DE-19
var rxPat = regexp.MustCompile(`^[A-Za-z]{2}-[0-9]{2}`)
if !rxPat.MatchString(vpnServerDomain) {
response = "Provide a validly formatted VPN server (hint: output from `vpns`)"
} else {
response = updateVpnPiTunnel(vpnServerDomain)
}
}
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "version" {
fingerprint := getDeployFingerprint("/root/.ssh/id_ed25519-cert.pub")
response := ":github: <https://github.com/ackersonde/bender-slackbot/actions/runs/" +
githubRunID + "|" + githubRunID + "> using :key: " + fingerprint
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "sw" {
response := ":partly_sunny_rain: <https://darksky.net/forecast/48.3028,11.3591/ca24/en#week|7-day forecast Schwabhausen>"
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "trans" || args[0] == "trand" || args[0] == "tranc" || args[0] == "tranp" {
response := torrentCommand(args)
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "mvv" {
response := "<" + mvvRoute("Schwabhausen", "München, Hauptbahnhof") + "|Going in>"
response += " | <" + mvvRoute("München, Hauptbahnhof", "Schwabhausen") + "|Going home>"
response += "\n" + fetchAktuelles()
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "www" {
fritzBox := ":fritzbox: <https://fritz.ackerson.de/|fritz.box> | "
fritzBox += ":traefik: <https://monitor.ackerson.de/dashboard/#/ | traefik> | "
pi4 := ":k8s: <https://dash.ackerson.de/#/overview?namespace=default|k8s>\n"
pi4 += ":pihole: <https://hole.ackerson.de/admin/|pi.hole> | "
vpnpi := ":transmission: <https://transmission.ackerson.de/transmission/web/|trans> | "
vpnpi += ":plex: <https://plex.ackerson.de/web/index.html#|plex>\n"
response := fritzBox + pi4 + vpnpi
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "key" {
response := getBendersCurrentSSHCert()
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else if args[0] == "help" {
response :=
":ethereum: `crypto`: Current cryptocurrency stats :lumens:\n" +
":sleuth_or_spy: `pgp`: PGP keys\n" +
":sun_behind_rain_cloud: `sw`: Schwabhausen weather\n" +
":mvv: `mvv`: Status | Trip In | Trip Home\n" +
":baseball: `bb <YYYY-MM-DD>`: show baseball games from given date (default yesterday)\n" +
//":do_droplet: `do|dd <id>`: show|delete DigitalOcean droplet(s)\n" +
":wireguard: `wg[s|u|d]`: [S]how status, [U]p or [D]own wireguard tunnel\n" +
":protonvpn: `vpn[s|c]`: [S]how status of VPN on :raspberry_pi:, [C]hange VPN to best in given country or " + VPNCountry + "\n" +
":pirate_bay: `torq <search term>`\n" +
":transmission: `tran[c|p|s|d]`: [C]reate <URL>, [P]aused <URL>, [S]tatus, [D]elete <ID> torrents on :raspberry_pi:\n" +
":movie_camera: `mv " + piPlexPath + "/torrents/<filename> [movies|tv/(<path>)]`\n" +
":youtube: `yt <video url>`: Download Youtube video to Papa's handy\n" +
":floppy_disk: `fsck`: show disk space on :raspberry_pi:\n" +
":bar_chart: `pi`: Stats of various :raspberry_pi:s\n" +
":github: `version`: Which build/deploy is this Bender bot?\n" +
":earth_americas: `www`: Show various internal links\n" +
":copyright: `scpxl <URL>`: scp URL file to Pops4XL\n"
api.PostMessage(event.Channel, slack.MsgOptionText(response, true), params)
} else if event.User != "" {
response := "whaddya say <@" + event.Username + ">? Try `help` instead..."
api.PostMessage(event.Channel, slack.MsgOptionText(response, false), params)
} else {
Logger.Printf("No Command found: %s", event.Text)
}
}
func getBendersCurrentSSHCert() string {
response := ""
out, err := exec.Command("ssh-keygen", "-L", "-f", "/root/.ssh/id_ed25519-cert.pub").Output()
if err != nil {
response += err.Error()
} else {
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
text := strings.Trim(scanner.Text(), " ")
if strings.HasPrefix(text, "Serial:") {
response = text
continue
} else if strings.HasPrefix(text, "Valid:") {
valid := text
// Valid: from 2021-02-02T13:44:00 to 2021-03-09T13:45:01
re := regexp.MustCompile(`Valid: from (?P<start>.*) to (?P<expire>.*)`)
matches := re.FindAllStringSubmatch(text, -1)
names := re.SubexpNames()
m := map[string]string{}
if len(matches) > 0 {
for i, n := range matches[0] {
m[names[i]] = n
}
if len(m) > 1 {
expiry, err := time.Parse("2006-01-02T15:04:05", m["expire"])
if err != nil {
Logger.Printf("Unable to parse expiry date: %s", m["expire"])
} else {
today := time.Now()
if expiry.Before(today) {
valid += "\n" + ":rotating_light: Cert is expired! Please check `/var/log/gen_new_deploy_keys.log` and possibly rerun `/home/ubuntu/my-ca/gen_new_deploy_keys.sh` on pi4." +
"\nOr just <https://github.com/ackersonde/bender-slackbot/actions|redeploy bender> ..."
} else {
daysValid := expiry.Sub(today).Hours() / 24
valid += "\nSSH Certificate valid for " + strconv.FormatFloat(daysValid, 'f', 0, 64) + " days"
}
}
} else {
Logger.Printf("Unable to parse validity: %s", valid)
}
} else {
Logger.Printf("ERR: PUB CERT invalid date: %s", valid)
}
response += "\n" + valid
break
}
}
}
return response
}
func fetchAktuelles() string {
rndString := strconv.FormatInt(time.Now().UnixNano(), 10)
url := "https://db-streckenagent.hafas.de/newsletter/gate?rnd=" + rndString
// Goto https://www.s-bahn-muenchen.de/s_muenchen/view/service/aktuelle_betriebslage.shtml w/ DevTools enabled
// inspect REQs like https://db-streckenagent.hafas.de/newsletter/gate?rnd=xyz
// as I expect the post data below may change regularly :(
var postData = []byte(`{"id":"ssww7rjiiqci9m88","ver":"1.25","lang":"deu","auth":{"type":"AID","aid":"da39a3ee5e6b4"},"client":{"id":"HAFAS","type":"WEB","name":"webapp","l":"vs_webapp"},"formatted":false,"svcReqL":[{"req":{"getChildren":true,"getParent":true,"maxNum":500,"himFltrL":[{"mode":"INC","type":"CH","value":"CUSTOM1"}],"sortL":["LMOD_DESC"]},"meth":"HimSearch","id":"1|1|"}]}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(postData))
if err != nil {
log.Printf("ERR prep request: %s", err.Error())
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Printf("ERR make request: %s", err.Error())
}
defer resp.Body.Close()
response, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("ERR read resp: %s", err.Error())
}
/* TEST response
response := []byte(`
{"ver":"1.25","lang":"deu","id":"ssww7rjiiqci9m88","err":"OK","graph":{"id":"standard","index":0},"subGraph":{"id":"global","index":0},"view":{"id":"standard","index":0,"type":"WGS84"},"svcResL":[{"id":"1|1|","meth":"HimSearch","err":"OK","res":{"common":{"locL":[{"lid":"A=1@O=München Ost@X=11604975@Y=48127437@U=80@L=8000262@","type":"S","name":"München Ost","icoX":0,"extId":"8000262","state":"F","crd":{"x":11604993,"y":48127302,"z":0},"pCls":447},{"lid":"A=1@O=München Donnersbergerbrücke@X=11536540@Y=48142620@U=80@L=8004128@","type":"S","name":"München Donnersbergerbrücke","icoX":1,"extId":"8004128","state":"F","crd":{"x":11537133,"y":48142683,"z":0},"pCls":56}],"prodL":[{"name":"S 1"},{"name":"S 6"},{"name":"S 7"},{"name":"S 8"},{"name":"S 2"}],"icoL":[{"res":"prod_ice","fg":{"r":255,"g":255,"b":255},"bg":{"r":40,"g":45,"b":55}},{"res":"prod_reg","fg":{"r":255,"g":255,"b":255},"bg":{"r":175,"g":180,"b":187}},{"res":"HIM1"}],"himMsgEdgeL":[{"icoCrd":{"x":11570757,"y":48135028}}],"himMsgCatL":[{"id":1}],"gTagL":["titleText","emailTitle","operationalSituationTitle","operationalSituation","email"]},"msgL":[{"hid":"RIS_HIM_FREETEXT_1080490","act":true,"head":"Bauarbeiten.","icoX":2,"prio":50,"fLocX":0,"tLocX":1,"prod":65535,"affProdRefL":[0,1,2,3,4],"src":99,"lModDate":"20200529","lModTime":"202744","sDate":"20200529","sTime":"223000","eDate":"20200601","eTime":"043000","sDaily":"000000","eDaily":"235900","comp":"Region Bayern","catRefL":[0],"pubChL":[{"name":"EMAIL","fDate":"20200529","fTime":"201500","tDate":"20200601","tTime":"043000"},{"name":"CUSTOM1","fDate":"20200529","fTime":"201500","tDate":"20200601","tTime":"043000"}],"edgeRefL":[0],"texts":[{"gTagXL":[0],"texts":[{"text":"Bauarbeiten."}]},{"gTagXL":[1,2],"texts":[{"text":"Stammstrecke: Bauarbeiten von Freitag, 29. Mai, 22.30 Uhr bis Montag, 01. Juni 2020, 4.30 Uhr zwischen München Ost und München-Pasing"}]},{"gTagXL":[3,4],"texts":[{"text":"Wegen Bauarbeiten zur 2.Stammstrecke kommt es von Freitag, 29. Mai (22:30 Uhr) durchgehend bis Montag, 1. Juni 2020 (4:30 Uhr) zwischen München Ost und München-Pasing zu Fahrplanänderungen mit Umleitungen und Haltausfällen auf fast allen S-Bahn-Linien. <br><br>Zwischen München Ost und München-Pasing verkehren nur die Linien S 6 und S 7 regulär durch die Stammstrecke.<br>Zwischen München-Ost und Hackerbrücke besteht am Samstag, jeweils von 9 bis 1 Uhr und am Sonntag, jeweils von 11 bis 21 Uhr ein Pendelverkehr im 20-Minuten-Takt.<br><br>Weitere Informationen, sowie die Fahrpläne der einzelnen Linien finden Sie unter https://t1p.de/94jj"}]}]}]}}]}`)
*/
svcResL := jsoniter.Get(response, "svcResL", 0).ToString()
res := jsoniter.Get([]byte(svcResL), "res").ToString()
common := jsoniter.Get([]byte(res), "common").ToString()
if common == "{}" {
return "Aktuell liegen uns keine Meldungen vor."
}
aktuell := ""
lines := strings.ToLower(jsoniter.Get([]byte(common), "prodL", '*').ToString())
if lines != "" {
effectedTrains := []map[string]string{}
effectedTrainLogos := ""
err2 := json.Unmarshal([]byte(lines), &effectedTrains)
if err2 != nil {
log.Printf("ERR parsing trains: %s", err2.Error())
} else {
for _, train := range effectedTrains {
logo := strings.Replace(train["name"], " ", "", 1)
effectedTrainLogos += fmt.Sprintf(":mvv_" + logo + ": ")
}
effectedTrainLogos += "\n"
aktuell = fmt.Sprintf("%s\n", effectedTrainLogos)
}
}
msgL := jsoniter.Get([]byte(res), "msgL", 0).ToString()
titleTexts := jsoniter.Get([]byte(msgL), "texts", 1).ToString()
gTagXL1 := jsoniter.Get([]byte(titleTexts), "texts", 0).ToString()
titleText := jsoniter.Get([]byte(gTagXL1), "text").ToString()
subjectTexts := jsoniter.Get([]byte(msgL), "texts", 2).ToString()
gTagXL2 := jsoniter.Get([]byte(subjectTexts), "texts", 0).ToString()
subjectText := jsoniter.Get([]byte(gTagXL2), "text").ToString()
subjectText = strings.ReplaceAll(subjectText, "<br>", "\n")
aktuell += fmt.Sprintf("%s\n\n%s\n\n", titleText, subjectText)
lModDate := jsoniter.Get([]byte(msgL), "lModDate").ToString()
lModTime := jsoniter.Get([]byte(msgL), "lModTime").ToString()
if lModDate != "" && lModTime != "" {
lastUpdate, _ := time.Parse("20060102150405", lModDate+lModTime)
aktuell += fmt.Sprintf("_update von %s_", lastUpdate.Format("02-Jan-2006 15:04"))
}
return aktuell
}
func mvvRoute(origin string, destination string) string {
loc, _ := time.LoadLocation("Europe/Berlin")
date := time.Now().In(loc)
yearObj := date.Year()
monthObj := int(date.Month())
dayObj := date.Day()
hourObj := date.Hour()
minuteObj := date.Minute()
month := strconv.Itoa(monthObj)
hour := strconv.Itoa(hourObj)
day := strconv.Itoa(dayObj)
minute := strconv.Itoa(minuteObj)
year := strconv.Itoa(yearObj)
return "http://efa.mvv-muenchen.de/mvv/XSLT_TRIP_REQUEST2?&language=de" +
"&anyObjFilter_origin=0&sessionID=0&itdTripDateTimeDepArr=dep&type_destination=any" +
"&itdDateMonth=" + month + "&itdTimeHour=" + hour + "&anySigWhenPerfectNoOtherMatches=1" +
"&locationServerActive=1&name_origin=" + origin + "&itdDateDay=" + day + "&type_origin=any" +
"&name_destination=" + destination + "&itdTimeMinute=" + minute + "&Session=0&stateless=1" +
"&SpEncId=0&itdDateYear=" + year
}