Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RIPE Atlas Commands #459

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/innogames/slack-bot/v2/command/pool"
"github.com/innogames/slack-bot/v2/command/pullrequest"
"github.com/innogames/slack-bot/v2/command/queue"
"github.com/innogames/slack-bot/v2/command/ripeatlas"
"github.com/innogames/slack-bot/v2/command/weather"
)

Expand Down Expand Up @@ -73,6 +74,9 @@ func GetCommands(slackClient client.SlackClient, cfg config.Config) *bot.Command
// openai/chatgpt
commands.Merge(openai.GetCommands(base, &cfg))

// Ripe Atlas
commands.Merge(ripeatlas.GetCommands(base, &cfg))

return commands
}

Expand Down
182 changes: 182 additions & 0 deletions command/ripeatlas/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package ripeatlas

import (
"encoding/json"
"fmt"
"time"
)

type CreditsResponse struct {
CurrentBalance int `json:"current_balance"`
CreditChecked bool `json:"credit_checked"`
MaxDailyCredits int `json:"max_daily_credits"`
EstimatedDailyIncome int `json:"estimated_daily_income"`
EstimatedDailyExpenditure int `json:"estimated_daily_expenditure"`
EstimatedDailyBalance int `json:"estimated_daily_balance"`
CalculationTime string `json:"calculation_time"`
EstimatedRunoutSeconds any `json:"estimated_runout_seconds"`
PastDayMeasurementResults int `json:"past_day_measurement_results"`
PastDayCreditsSpent int `json:"past_day_credits_spent"`
LastDateDebited string `json:"last_date_debited"`
LastDateCredited string `json:"last_date_credited"`
IncomeItems string `json:"income_items"`
ExpenseItems string `json:"expense_items"`
Transactions string `json:"transactions"`
}

type MeasurementsResponse struct {
Count int `json:"count,omitempty"`
Next string `json:"next,omitempty"`
Previous string `json:"previous,omitempty"`
Measurements []Measurement `json:"results"`
}

type MeasurementResult struct {
Measurements []int `json:"measurements"`
}

type Measurement struct {
AddressFamily int `json:"af"`
CreationTime int `json:"creation_time"`
CreditsPerResult int `json:"credits_per_result"`
Description string `json:"description"`
EstimatedResultsPerDay int `json:"estimated_results_per_day"`
Group string `json:"group"`
GroupID int64 `json:"group_id"`
ID int `json:"id"`
InWifiGroup bool `json:"in_wifi_group"`
IncludeProbeID bool `json:"include_probe_id"`
Interval int `json:"interval"`
IsAllScheduled bool `json:"is_all_scheduled"`
IsOneoff bool `json:"is_oneoff"`
IsPublic bool `json:"is_public"`
PacketInterval int64 `json:"packet_interval"`
Packets int `json:"packets"`
ParticipantCount int64 `json:"participant_count"`
ProbesRequested int64 `json:"probes_requested"`
ProbesScheduled int64 `json:"probes_scheduled"`
ResolveOnProbe bool `json:"resolve_on_probe"`
ResolvedIps string `json:"resolved_ips"`
Result string `json:"result"`
Size int64 `json:"size"`
Spread int64 `json:"spread"`
StartTime int `json:"start_time"`
Status MeasurementStatus `json:"status"`
StopTime int `json:"stop_time"`
Tags []string `json:"tags"`
Target string `json:"target"`
TargetAsn int64 `json:"target_asn"`
TargetIP string `json:"target_ip"`
TargetPrefix string `json:"target_prefix"`
Type string `json:"type"`
}
type MeasurementStatus struct {
Name string `json:"name"`
ID int `json:"id"`
When any `json:"when"`
}

type MeasurementRequest struct {
Definitions []MeasurementDefinition `json:"definitions"`
Probes []Probes `json:"probes"`
IsOneOff bool `json:"is_oneoff"`
}
type MeasurementDefinition struct {
Target string `json:"target,omitempty"`
Af int `json:"af,omitempty"`
ResponseTimeout int `json:"response_timeout,omitempty"`
Description string `json:"description,omitempty"`
Protocol string `json:"protocol,omitempty"`
ResolveOnProbe bool `json:"resolve_on_probe,omitempty"`
Packets int `json:"packets,omitempty"`
Size int `json:"size,omitempty"`
FirstHop int `json:"first_hop,omitempty"`
MaxHops int `json:"max_hops,omitempty"`
Paris int `json:"paris,omitempty"`
DestinationOptionSize int `json:"destination_option_size,omitempty"`
HopByHopOptionSize int `json:"hop_by_hop_option_size,omitempty"`
DontFragment bool `json:"dont_fragment,omitempty"`
SkipDNSCheck bool `json:"skip_dns_check,omitempty"`
Type string `json:"type,omitempty"`
IsPublic bool `json:"is_public"`
}
type Probes struct {
Type string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
Requested int `json:"requested,omitempty"`
}

type StreamingResponse struct {
Type string `json:"type"`
Payload StreamingResponsePayload `json:"payload"`
}

func (sr *StreamingResponse) UnmarshalJSON(b []byte) error {
a := []interface{}{&sr.Type, &sr.Payload}
return json.Unmarshal(b, &a)
}

type StreamingResponsePayload struct {
Fw int `json:"fw,omitempty"`
Mver string `json:"mver,omitempty"`
Lts int `json:"lts,omitempty"`
Endtime int `json:"endtime,omitempty"`
DstName string `json:"dst_name,omitempty"`
DstAddr string `json:"dst_addr,omitempty"`
SrcAddr string `json:"src_addr,omitempty"`
Proto string `json:"proto,omitempty"`
Af int `json:"af,omitempty"`
Size int `json:"size,omitempty"`
ParisID int `json:"paris_id,omitempty"`
Result []PayloadResult `json:"result,omitempty"`
MsmID int `json:"msm_id,omitempty"`
PrbID int `json:"prb_id,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
MsmName string `json:"msm_name,omitempty"`
From string `json:"from,omitempty"`
Type string `json:"type,omitempty"`
GroupID int `json:"group_id,omitempty"`
}

func (srp StreamingResponsePayload) String() string {
// Start: 2023-08-03T14:01:07Z
// HOST: 2a02:1811:c1c:7800:a62b:b0ff:fef1:5062 Loss% Last
// 1 . AS0 172.20.0.1 0% 0.139
// 2 . AS0 172.26.4.1 0% 0.397
// 3 . AS0 192.168.144.1 0% 1.693
var text string
text += "```\n"
text += fmt.Sprintf("Start: %s\n", time.Unix(srp.Timestamp, 0))
text += fmt.Sprintf("HOST: %-40s Loss%% RTT\n", srp.SrcAddr)

for _, res := range srp.Result {
var from string
switch {
case len(res.Result[0].From) > 0:
from = res.Result[0].From
case len(res.Result[1].From) > 0:
from = res.Result[1].From
case len(res.Result[2].From) > 0:
from = res.Result[2].From
default:
from = "???"
}

text += fmt.Sprintf("%2d . %-40s %4d%% %7.3f %7.3f %7.3f\n", res.Hop, from, 0, res.Result[0].Rtt, res.Result[1].Rtt, res.Result[2].Rtt)
}

text += "```\n"
return text
}

type HopResult struct {
From string `json:"from,omitempty"`
TTL int `json:"ttl,omitempty"`
Size int `json:"size,omitempty"`
Rtt float64 `json:"rtt,omitempty"`
Loss string `json:"x,omitempty"`
}
type PayloadResult struct {
Hop int `json:"hop,omitempty"`
Result []HopResult `json:"result,omitempty"`
}
27 changes: 27 additions & 0 deletions command/ripeatlas/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ripeatlas

import (
"github.com/innogames/slack-bot/v2/bot"
"github.com/innogames/slack-bot/v2/bot/config"
)

var category = bot.Category{
Name: "RIPE Atlas",
Description: "Run queries against the RIPE Atlas API to debug network issues",
}

func GetCommands(base bot.BaseCommand, config *config.Config) bot.Commands {
var commands bot.Commands

cfg := loadConfig(config)
if !cfg.IsEnabled() {
return commands
}

commands.AddCommand(
&creditsCommand{base, cfg},
&tracerouteCommand{base, cfg},
)

return commands
}
33 changes: 33 additions & 0 deletions command/ripeatlas/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ripeatlas

import (
"time"

"github.com/innogames/slack-bot/v2/bot/config"
)

// Config configuration: API key to do API calls
type Config struct {
APIKey string `mapstructure:"api_key"`
APIURL string `mapstructure:"api_url"`
StreamURL string `mapstructure:"stream_url"`
UpdateInterval time.Duration `mapstructure:"update_interval"`
}

// IsEnabled checks if token is set
func (c *Config) IsEnabled() bool {
return c.APIKey != ""
}

var defaultConfig = Config{
APIURL: "https://atlas.ripe.net/api/v2",
StreamURL: "https://atlas-stream.ripe.net/stream/",
UpdateInterval: time.Second,
}

func loadConfig(config *config.Config) Config {
cfg := defaultConfig
_ = config.LoadCustom("ripeatlas", &cfg)

return cfg
}
76 changes: 76 additions & 0 deletions command/ripeatlas/credits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ripeatlas

import (
"encoding/json"
"fmt"
"net/http"

"github.com/innogames/slack-bot/v2/bot"
"github.com/innogames/slack-bot/v2/bot/matcher"
"github.com/innogames/slack-bot/v2/bot/msg"
"github.com/innogames/slack-bot/v2/client"
log "github.com/sirupsen/logrus"
)

type creditsCommand struct {
bot.BaseCommand
cfg Config
}

func (c *creditsCommand) GetMatcher() matcher.Matcher {
return matcher.NewGroupMatcher(
matcher.NewTextMatcher("credits", c.credits),
)
}

func (c *creditsCommand) credits(_ matcher.Result, message msg.Message) {
c.AddReaction(":coffee:", message)
defer c.RemoveReaction(":coffee:", message)

url := fmt.Sprintf("%s/credits", c.cfg.APIURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
c.ReplyError(message, fmt.Errorf("request creation returned an err: %w", err))
log.Errorf("request creation returned an err: %s", err)
return
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Key "+c.cfg.APIKey)

response, err := client.GetHTTPClient().Do(req)
if err != nil {
c.ReplyError(message, fmt.Errorf("API call returned an err: %w", err))
log.Errorf("API call returned an err: %s", err)
return
}
defer response.Body.Close()

if response.StatusCode >= 400 {
c.ReplyError(message, fmt.Errorf("API call returned an err: %d", response.StatusCode))
log.Errorf("API call returned an err: %d", response.StatusCode)
return
}

var result CreditsResponse
err = json.NewDecoder(response.Body).Decode(&result)
if err != nil {
c.ReplyError(message, err)
log.Errorf("%s", err)
return
}

text := fmt.Sprintf("Total credits remaining: %d", result.CurrentBalance)

c.SendMessage(message, text)
}

func (c *creditsCommand) GetHelp() []bot.Help {
return []bot.Help{
{
Command: "credits",
Description: "Query how many credits are available for this API key",
Category: category,
},
}
}
Loading
Loading