Skip to content

Commit

Permalink
feat: add location endpoint
Browse files Browse the repository at this point in the history
This makes it possible to see which endpoints are currently registered
and their location.
  • Loading branch information
crazybolillo committed Nov 8, 2024
1 parent 18cca56 commit 515507c
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ func serve(ctx context.Context) error {
cdr := handler.Cdr{Service: &service.Cdr{Cursor: pool}}
r.Mount("/cdr", cdr.Router())

location := handler.Location{Service: &service.Location{Cursor: pool}}
r.Mount("/locations", location.Router())

r.Mount("/metrics", promhttp.Handler())

listen := os.Getenv("LISTEN_ADDR")
Expand Down
45 changes: 45 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ definitions:
sid:
type: integer
type: object
model.Location:
properties:
address:
type: string
endpoint:
type: string
id:
type: string
user_agent:
type: string
type: object
model.LocationPage:
properties:
locations:
items:
$ref: '#/definitions/model.Location'
type: array
retrieved:
type: integer
total:
type: integer
type: object
model.NewEndpoint:
properties:
accountCode:
Expand Down Expand Up @@ -376,4 +398,27 @@ paths:
summary: Update the specified endpoint. Omitted or null fields will remain unchanged.
tags:
- endpoints
/locations:
get:
parameters:
- default: 0
description: Zero based page to fetch
in: query
name: page
type: integer
- default: 20
description: Max amount of results to be returned
in: query
name: pageSize
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.LocationPage'
summary: List endpoint locations. This is the same as PJSIP contacts.
tags:
- location
swagger: "2.0"
62 changes: 62 additions & 0 deletions internal/handler/location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package handler

import (
"encoding/json"
"github.com/crazybolillo/eryth/internal/query"
"github.com/crazybolillo/eryth/internal/service"
"github.com/go-chi/chi/v5"
"log/slog"
"net/http"
)

type Location struct {
Service *service.Location
}

func (l *Location) Router() chi.Router {
r := chi.NewRouter()
r.Get("/", l.list)

return r
}

// @Summary List endpoint locations. This is the same as PJSIP contacts.
// @Param page query int false "Zero based page to fetch" default(0)
// @Param pageSize query int false "Max amount of results to be returned" default(20)
// @Produce json
// @Success 200 {object} model.LocationPage
// @Tags location
// @Router /locations [get]
func (l *Location) list(w http.ResponseWriter, r *http.Request) {
page, err := query.GetIntOr(r.URL.Query(), "page", 0)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

pageSize, err := query.GetIntOr(r.URL.Query(), "pageSize", 25)
if err != nil || page < 0 || pageSize < 0 {
w.WriteHeader(http.StatusBadRequest)
return
}

res, err := l.Service.Paginate(r.Context(), page, pageSize)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to list locations", "path", r.URL.Path, "reason", err)
return
}

content, err := json.Marshal(res)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to marshall response", "path", r.URL.Path, "reason", err)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(content)
if err != nil {
slog.Error("Failed to write response", "path", r.URL.Path, "reason", err)
}
}
83 changes: 83 additions & 0 deletions internal/service/location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package service

import (
"context"
"errors"
"github.com/crazybolillo/eryth/internal/sqlc"
"github.com/crazybolillo/eryth/pkg/model"
"github.com/jackc/pgx/v5"
"net/url"
"strings"
)

type Location struct {
Cursor Cursor `json:"cursor"`
}

func locationParseRow(row sqlc.ListLocationsRow) model.Location {
location := model.Location{
Endpoint: row.Endpoint.String,
UserAgent: row.UserAgent.String,
}

id := strings.Split(row.ID, "@")
if len(id) != 2 {
location.ID = id[0]
} else {
location.ID = id[1]
}

var ua string
rawUA := strings.Replace(row.UserAgent.String, "^", "%", -1)
ua, err := url.QueryUnescape(rawUA)
if err != nil {
ua = rawUA
}
location.UserAgent = ua

var uri string
rawUri := strings.Replace(row.Uri.String, "^", "%", -1)
uri, err = url.QueryUnescape(rawUri)
if err != nil {
uri = rawUri
}

addr := strings.Split(uri, "@")
if len(addr) != 2 {
location.Address = addr[0]
} else {
location.Address = addr[1]
}

return location
}

func (l *Location) Paginate(ctx context.Context, page, size int) (model.LocationPage, error) {
queries := sqlc.New(l.Cursor)

rows, err := queries.ListLocations(ctx, sqlc.ListLocationsParams{
Limit: int32(size),
Offset: int32(page),
})
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return model.LocationPage{}, err
}

count, err := queries.CountLocations(ctx)
if err != nil {
return model.LocationPage{}, err
}

locations := make([]model.Location, len(rows))
for idx, row := range rows {
locations[idx] = locationParseRow(row)
}

res := model.LocationPage{
Locations: locations,
Total: count,
Retrieved: len(locations),
}

return res, nil
}
42 changes: 42 additions & 0 deletions internal/service/location_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package service

import (
"github.com/crazybolillo/eryth/internal/db"
"github.com/crazybolillo/eryth/internal/sqlc"
"github.com/crazybolillo/eryth/pkg/model"
"reflect"
"testing"
)

func TestLocationParseRow(t *testing.T) {
cases := []struct {
name string
row sqlc.ListLocationsRow
want model.Location
}{
{
name: "escaped",
row: sqlc.ListLocationsRow{
ID: "rando",
Endpoint: db.Text("kiwi"),
UserAgent: db.Text("LinphoneAndroid/5.2.5 (Galaxy S7 edge) LinphoneSDK/5.3.47 (tags/5.3.47^5E0)"),
Uri: db.Text("sip:[email protected]:45331^3Btransport=tcp"),
},
want: model.Location{
ID: "rando",
Endpoint: "kiwi",
UserAgent: "LinphoneAndroid/5.2.5 (Galaxy S7 edge) LinphoneSDK/5.3.47 (tags/5.3.47^0)",
Address: "192.168.100.24:45331;transport=tcp",
},
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got := locationParseRow(tt.row)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
59 changes: 59 additions & 0 deletions internal/sqlc/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions pkg/model/location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package model

type Location struct {
ID string `json:"id"`
Endpoint string `json:"endpoint"`
Address string `json:"address"`
UserAgent string `json:"user_agent"`
}

type LocationPage struct {
Total int64 `json:"total"`
Retrieved int `json:"retrieved"`
Locations []Location `json:"locations"`
}
13 changes: 13 additions & 0 deletions queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,16 @@ LIMIT $1 OFFSET $2;

-- name: CountCallRecords :one
SELECT COUNT(*) FROM cdr;

-- name: ListLocations :many
SELECT
id,
endpoint,
user_agent,
uri
FROM
ps_contacts
LIMIT $1 OFFSET $2;

-- name: CountLocations :one
SELECT COUNT(*) FROM ps_contacts;

0 comments on commit 515507c

Please sign in to comment.