add channels api routes

This commit is contained in:
Enot (ded) Skelly committed 2026-05-25 14:58:50 -07:00
1 parent edea0516a0
commit b5c9ca656f
6 files changed
+455 -76

No files matched your search

+22 -7
View File
@@ -245,7 +245,7 @@ SELECT * FROM channels ORDER BY last_seen DESC LIMIT $1;
-- name: GetChannelsByHash :many
-- Returns all channels for a given hash (may be multiple on hash collision).
SELECT * FROM channels WHERE channel_hash = $1 ORDER BY last_seen DESC;
SELECT * FROM channels WHERE channel_hash = $1 ORDER BY last_seen DESC LIMIT $2;
-- name: GetChannelByHashAndFingerprint :one
SELECT * FROM channels WHERE channel_hash = $1 AND key_fingerprint = $2;
@@ -253,20 +253,35 @@ SELECT * FROM channels WHERE channel_hash = $1 AND key_fingerprint = $2;
-- name: GetChannelByHashtag :one
SELECT * FROM channels WHERE hashtag = $1;
-- name: GetChannelByID :one
SELECT * FROM channels WHERE id = $1;
-- ============================================================
-- CHANNEL MESSAGES
-- ============================================================
-- name: InsertChannelMessage :exec
INSERT INTO channel_messages (channel_id, packet_hash, sender_name, sender_pubkey, content, sent_at)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO channel_messages (channel_id, packet_hash, sender_name, content, sent_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (packet_hash) DO NOTHING;
-- name: ListChannelMessages :many
SELECT * FROM channel_messages
WHERE channel_id = $1
AND ($2::timestamptz IS NULL OR sent_at >= $2)
ORDER BY sent_at DESC
SELECT cm.*, encode(p.packet_hash, 'hex') as packet_hash_hex, c.channel_hash
FROM channel_messages cm
JOIN packets p ON p.packet_hash = cm.packet_hash
JOIN channels c ON c.id = cm.channel_id
WHERE cm.channel_id = $1
AND ($2::timestamptz IS NULL OR cm.sent_at >= $2)
ORDER BY cm.sent_at DESC
LIMIT $3;
-- name: ListChannelMessagesByHash :many
SELECT cm.*, c.channel_hash FROM channel_messages cm
JOIN channels c ON c.id = cm.channel_id
WHERE c.channel_hash = $1
AND ($2::timestamptz IS NULL OR cm.sent_at >= $2)
ORDER BY cm.sent_at DESC
LIMIT $3;
-- ============================================================
+118 -20
View File
@@ -63,13 +63,41 @@ func (q *Queries) GetChannelByHashtag(ctx context.Context, hashtag *string) (Cha
return i, err
}
const getChannelsByHash = `-- name: GetChannelsByHash :many
SELECT id, channel_hash, key_fingerprint, name, hashtag, is_hashtag, is_public, key_known, first_seen, last_seen, message_count FROM channels WHERE channel_hash = $1 ORDER BY last_seen DESC
const getChannelByID = `-- name: GetChannelByID :one
SELECT id, channel_hash, key_fingerprint, name, hashtag, is_hashtag, is_public, key_known, first_seen, last_seen, message_count FROM channels WHERE id = $1
`
func (q *Queries) GetChannelByID(ctx context.Context, id int32) (Channel, error) {
row := q.db.QueryRow(ctx, getChannelByID, id)
var i Channel
err := row.Scan(
&i.ID,
&i.ChannelHash,
&i.KeyFingerprint,
&i.Name,
&i.Hashtag,
&i.IsHashtag,
&i.IsPublic,
&i.KeyKnown,
&i.FirstSeen,
&i.LastSeen,
&i.MessageCount,
)
return i, err
}
const getChannelsByHash = `-- name: GetChannelsByHash :many
SELECT id, channel_hash, key_fingerprint, name, hashtag, is_hashtag, is_public, key_known, first_seen, last_seen, message_count FROM channels WHERE channel_hash = $1 ORDER BY last_seen DESC LIMIT $2
`
type GetChannelsByHashParams struct {
ChannelHash []byte `json:"channel_hash"`
Limit int32 `json:"limit"`
}
// Returns all channels for a given hash (may be multiple on hash collision).
func (q *Queries) GetChannelsByHash(ctx context.Context, channelHash []byte) ([]Channel, error) {
rows, err := q.db.Query(ctx, getChannelsByHash, channelHash)
func (q *Queries) GetChannelsByHash(ctx context.Context, arg GetChannelsByHashParams) ([]Channel, error) {
rows, err := q.db.Query(ctx, getChannelsByHash, arg.ChannelHash, arg.Limit)
if err != nil {
return nil, err
}
@@ -411,18 +439,17 @@ func (q *Queries) GetTopNodes(ctx context.Context, arg GetTopNodesParams) ([]MvT
const insertChannelMessage = `-- name: InsertChannelMessage :exec
INSERT INTO channel_messages (channel_id, packet_hash, sender_name, sender_pubkey, content, sent_at)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO channel_messages (channel_id, packet_hash, sender_name, content, sent_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (packet_hash) DO NOTHING
`
type InsertChannelMessageParams struct {
ChannelID int32 `json:"channel_id"`
PacketHash []byte `json:"packet_hash"`
SenderName *string `json:"sender_name"`
SenderPubkey []byte `json:"sender_pubkey"`
Content *string `json:"content"`
SentAt pgtype.Timestamptz `json:"sent_at"`
ChannelID int32 `json:"channel_id"`
PacketHash []byte `json:"packet_hash"`
SenderName *string `json:"sender_name"`
Content *string `json:"content"`
SentAt pgtype.Timestamptz `json:"sent_at"`
}
// ============================================================
@@ -433,7 +460,6 @@ func (q *Queries) InsertChannelMessage(ctx context.Context, arg InsertChannelMes
arg.ChannelID,
arg.PacketHash,
arg.SenderName,
arg.SenderPubkey,
arg.Content,
arg.SentAt,
)
@@ -531,10 +557,13 @@ func (q *Queries) InsertObservation(ctx context.Context, arg InsertObservationPa
}
const listChannelMessages = `-- name: ListChannelMessages :many
SELECT id, channel_id, packet_hash, sender_name, sender_pubkey, content, sent_at FROM channel_messages
WHERE channel_id = $1
AND ($2::timestamptz IS NULL OR sent_at >= $2)
ORDER BY sent_at DESC
SELECT cm.id, cm.channel_id, cm.packet_hash, cm.sender_name, cm.sender_pubkey, cm.content, cm.sent_at, encode(p.packet_hash, 'hex') as packet_hash_hex, c.channel_hash
FROM channel_messages cm
JOIN packets p ON p.packet_hash = cm.packet_hash
JOIN channels c ON c.id = cm.channel_id
WHERE cm.channel_id = $1
AND ($2::timestamptz IS NULL OR cm.sent_at >= $2)
ORDER BY cm.sent_at DESC
LIMIT $3
`
@@ -544,15 +573,27 @@ type ListChannelMessagesParams struct {
Limit int32 `json:"limit"`
}
func (q *Queries) ListChannelMessages(ctx context.Context, arg ListChannelMessagesParams) ([]ChannelMessage, error) {
type ListChannelMessagesRow struct {
ID int64 `json:"id"`
ChannelID int32 `json:"channel_id"`
PacketHash []byte `json:"packet_hash"`
SenderName *string `json:"sender_name"`
SenderPubkey []byte `json:"sender_pubkey"`
Content *string `json:"content"`
SentAt pgtype.Timestamptz `json:"sent_at"`
PacketHashHex string `json:"packet_hash_hex"`
ChannelHash []byte `json:"channel_hash"`
}
func (q *Queries) ListChannelMessages(ctx context.Context, arg ListChannelMessagesParams) ([]ListChannelMessagesRow, error) {
rows, err := q.db.Query(ctx, listChannelMessages, arg.ChannelID, arg.Column2, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ChannelMessage{}
items := []ListChannelMessagesRow{}
for rows.Next() {
var i ChannelMessage
var i ListChannelMessagesRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
@@ -561,6 +602,63 @@ func (q *Queries) ListChannelMessages(ctx context.Context, arg ListChannelMessag
&i.SenderPubkey,
&i.Content,
&i.SentAt,
&i.PacketHashHex,
&i.ChannelHash,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listChannelMessagesByHash = `-- name: ListChannelMessagesByHash :many
SELECT cm.id, cm.channel_id, cm.packet_hash, cm.sender_name, cm.sender_pubkey, cm.content, cm.sent_at, c.channel_hash FROM channel_messages cm
JOIN channels c ON c.id = cm.channel_id
WHERE c.channel_hash = $1
AND ($2::timestamptz IS NULL OR cm.sent_at >= $2)
ORDER BY cm.sent_at DESC
LIMIT $3
`
type ListChannelMessagesByHashParams struct {
ChannelHash []byte `json:"channel_hash"`
Column2 pgtype.Timestamptz `json:"column_2"`
Limit int32 `json:"limit"`
}
type ListChannelMessagesByHashRow struct {
ID int64 `json:"id"`
ChannelID int32 `json:"channel_id"`
PacketHash []byte `json:"packet_hash"`
SenderName *string `json:"sender_name"`
SenderPubkey []byte `json:"sender_pubkey"`
Content *string `json:"content"`
SentAt pgtype.Timestamptz `json:"sent_at"`
ChannelHash []byte `json:"channel_hash"`
}
func (q *Queries) ListChannelMessagesByHash(ctx context.Context, arg ListChannelMessagesByHashParams) ([]ListChannelMessagesByHashRow, error) {
rows, err := q.db.Query(ctx, listChannelMessagesByHash, arg.ChannelHash, arg.Column2, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListChannelMessagesByHashRow{}
for rows.Next() {
var i ListChannelMessagesByHashRow
if err := rows.Scan(
&i.ID,
&i.ChannelID,
&i.PacketHash,
&i.SenderName,
&i.SenderPubkey,
&i.Content,
&i.SentAt,
&i.ChannelHash,
); err != nil {
return nil, err
}
+142 -1
View File
@@ -6,7 +6,9 @@ package db
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"time"
sqlc "github.com/MeshCore-Tower/tower-server/db/sqlc"
"github.com/MeshCore-Tower/tower-server/internal/api"
@@ -32,7 +34,11 @@ func (s *Store) UpsertObserver(ctx context.Context, pubkey []byte) (uuid.UUID, s
if err != nil {
return uuid.Nil, "", err
}
return row.ID, *row.DisplayName, err
displayName := ""
if row.DisplayName != nil {
displayName = *row.DisplayName
}
return row.ID, displayName, err
}
// UpsertObserverBroker records that this observer was seen on brokerName.
@@ -348,3 +354,138 @@ func (s *Store) UpsertRegionIATA(ctx context.Context, regionID int32, iata strin
Iata: iata,
})
}
// ListChannels returns a summary list of all known channels ordered by last seen.
// Includes both hashtag-derived and explicit key channels.
// Channels with unknown keys are included with KeyKnown=false.
// Filters on hash if provided, this is the hex channel hash
func (s *Store) ListChannels(ctx context.Context, limit int32, hash []byte) ([]api.ChannelSummary, error) {
var rows []sqlc.Channel
if hash != nil {
r, err := s.q.GetChannelsByHash(ctx, sqlc.GetChannelsByHashParams{
ChannelHash: hash,
Limit: limit,
})
if err != nil {
return nil, err
}
rows = r
} else {
r, err := s.q.ListChannels(ctx, limit)
if err != nil {
return nil, err
}
rows = r
}
channels := make([]api.ChannelSummary, 0, len(rows))
for _, v := range rows {
channels = append(channels, api.ChannelSummary{
ID: int(v.ID),
Name: v.Name,
ChannelHash: hex.EncodeToString(v.ChannelHash),
LastSeen: v.LastSeen.Time.Format(time.RFC3339),
IsHashtag: v.IsHashtag != nil && *v.IsHashtag,
KeyKnown: v.KeyKnown != nil && *v.KeyKnown,
})
}
return channels, nil
}
// GetChannel returns full detail for a single channel by its integer ID.
// Returns nil, pgx.ErrNoRows if the channel is not found.
func (s *Store) GetChannel(ctx context.Context, channelID int32) (*api.Channel, error) {
row, err := s.q.GetChannelByID(ctx, channelID)
if err != nil {
return nil, err
}
channel := api.Channel{
ChannelSummary: api.ChannelSummary{
ID: int(row.ID),
Name: row.Name,
ChannelHash: hex.EncodeToString(row.ChannelHash),
LastSeen: row.LastSeen.Time.Format(time.RFC3339),
IsHashtag: row.IsHashtag != nil && *row.IsHashtag,
KeyKnown: row.KeyKnown != nil && *row.KeyKnown,
},
Hashtag: row.Hashtag,
MessageCount: 0,
}
if row.MessageCount != nil {
channel.MessageCount = *row.MessageCount
}
if row.IsHashtag != nil && *row.IsHashtag && row.KeyFingerprint != nil {
fp := hex.EncodeToString(row.KeyFingerprint)
channel.KeyFingerprint = &fp
}
return &channel, nil
}
// ListChannelMessages returns paginated messages for a channel identified by its integer ID.
// Used by the /channels/{id}/messages endpoint.
// Pass a zero time.Time for since to return all messages up to limit.
func (s *Store) ListChannelMessages(ctx context.Context, channelID int32, since time.Time, limit int32) ([]api.ChannelMessage, error) {
rows, err := s.q.ListChannelMessages(ctx, sqlc.ListChannelMessagesParams{
ChannelID: channelID,
Column2: pgtype.Timestamptz{Time: since, Valid: !since.IsZero()},
Limit: limit,
})
if err != nil {
return nil, err
}
messages := make([]api.ChannelMessage, 0, len(rows))
for _, v := range rows {
senderName := ""
if v.SenderName != nil {
senderName = *v.SenderName
}
content := ""
if v.Content != nil {
content = *v.Content
}
messages = append(messages, api.ChannelMessage{
ID: v.ID,
PacketHash: v.PacketHashHex,
ChannelHash: hex.EncodeToString(v.ChannelHash),
SenderName: senderName,
Content: content,
SentAt: v.SentAt.Time.Format(time.RFC3339),
})
}
return messages, nil
}
// ListChannelMessagesByHash returns paginated messages for all channels matching the given hash.
// Used by the /messages?hash= endpoint. May return messages from multiple channels
// if the hash collides across different keys.
// Pass a zero time.Time for since to return all messages up to limit.
func (s *Store) ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32) ([]api.ChannelMessage, error) {
rows, err := s.q.ListChannelMessagesByHash(ctx, sqlc.ListChannelMessagesByHashParams{
ChannelHash: hash,
Column2: pgtype.Timestamptz{Time: since, Valid: !since.IsZero()},
Limit: limit,
})
if err != nil {
return nil, err
}
messages := make([]api.ChannelMessage, 0, len(rows))
for _, v := range rows {
senderName := ""
if v.SenderName != nil {
senderName = *v.SenderName
}
content := ""
if v.Content != nil {
content = *v.Content
}
messages = append(messages, api.ChannelMessage{
ID: v.ID,
ChannelHash: hex.EncodeToString(v.ChannelHash),
PacketHash: hex.EncodeToString(v.PacketHash),
SenderName: senderName,
Content: content,
SentAt: v.SentAt.Time.Format(time.RFC3339),
})
}
return messages, nil
}
+115 -42
View File
@@ -1,59 +1,132 @@
package handlers
import (
"encoding/hex"
"net/http"
"strconv"
"time"
"github.com/MeshCore-Tower/tower-server/internal/api"
"github.com/go-chi/chi/v5"
)
// ChannelsRouter mounts all /channels routes onto a subrouter.
//
// GET /channels → ListChannels
// GET /channels/{channelHash} → GetChannel
// GET /channels/{channelHash}/messages → ListChannelMessages
func ChannelsRouter() http.Handler {
// GET /channels → ListChannels
// GET /channels/{channelID} → GetChannel
// GET /channels/{channelID}/messages → ListChannelMessages
func ChannelsRouter(reader api.Reader) http.Handler {
r := chi.NewRouter()
r.Get("/", ListChannels)
// ListChannels handles GET /api/v1/channels
//
// Query params (all optional):
//
// hash=<hex>
// limit=50
// cursor=<opaque>
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
var limit int64 = 50
if limitParam := r.URL.Query().Get("limit"); limitParam != "" {
l, err := strconv.ParseInt(limitParam, 10, 32)
if err != nil {
respond(w, http.StatusBadRequest, map[string]string{"error": "limit must be an integer"})
return
} else {
limit = l
}
}
var channels []api.ChannelSummary
var err error
if hash := r.URL.Query().Get("hash"); hash != "" {
hashHex, decodeErr := hex.DecodeString(hash)
if decodeErr != nil {
respond(w, http.StatusBadRequest, map[string]string{"error": "invalid channel hash"})
return
}
if len(hashHex) != 1 {
respond(w, http.StatusBadRequest, map[string]string{"error": "hash must be a single hex byte"})
return
}
channels, err = reader.ListChannels(r.Context(), int32(limit), hashHex)
} else {
channels, err = reader.ListChannels(r.Context(), int32(limit), nil)
}
if err != nil {
respond(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
return
}
respond(w, http.StatusOK, channels)
})
r.Route("/{channelHash}", func(r chi.Router) {
r.Get("/", GetChannel)
r.Get("/messages", ListChannelMessages)
r.Route("/{channelID}", func(r chi.Router) {
// GetChannel handles GET /api/v1/channels/{channelID}
//
// Returns channel detail including key for hashtag channels and message count.
// Other channel keys are server-side config; key material is never exposed via the API.
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
var id int64
if channelID := chi.URLParam(r, "channelID"); channelID != "" {
i, err := strconv.ParseInt(channelID, 10, 32)
if err != nil {
respond(w, http.StatusBadRequest, map[string]string{"error": "channelID should be an int 32"})
return
}
id = i
}
channel, err := reader.GetChannel(r.Context(), int32(id))
if err != nil {
respond(w, http.StatusNotFound, map[string]string{"error": "channel not found"})
return
}
respond(w, http.StatusOK, channel)
})
// ListChannelMessages handles GET /api/v1/channels/{channelID}/messages
//
// Query params (all optional):
//
// since=<epoch ms>
// limit=50
// cursor=<opaque>
//
// Returns paginated decrypted channel messages.
r.Get("/messages", func(w http.ResponseWriter, r *http.Request) {
var id int64
if channelID := chi.URLParam(r, "channelID"); channelID != "" {
i, err := strconv.ParseInt(channelID, 10, 32)
if err != nil {
respond(w, http.StatusBadRequest, map[string]string{"error": "channelID should be an int 32"})
return
}
id = i
}
var limit int64 = 50
if limitParam := r.URL.Query().Get("limit"); limitParam != "" {
l, err := strconv.ParseInt(limitParam, 10, 32)
if err != nil {
respond(w, http.StatusBadRequest, map[string]string{"error": "limit must be an integer"})
return
} else {
limit = l
}
}
var since time.Time
if sinceParam := r.URL.Query().Get("since"); sinceParam != "" {
ms, err := strconv.ParseInt(sinceParam, 10, 64)
if err != nil {
respond(w, http.StatusBadRequest, map[string]string{"error": "since must be epoch milliseconds"})
return
}
since = time.UnixMilli(ms)
}
messages, err := reader.ListChannelMessages(r.Context(), int32(id), since, int32(limit))
if err != nil {
respond(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
return
}
respond(w, http.StatusOK, messages)
})
})
return r
}
// ListChannels handles GET /api/v1/channels
//
// Query params (all optional):
// limit=50
func ListChannels(w http.ResponseWriter, r *http.Request) {
// TODO: query channels ORDER BY last_seen DESC, write JSON response.
w.WriteHeader(http.StatusNotImplemented)
}
// GetChannel handles GET /api/v1/channels/{channelHash}
//
// Returns channel detail including key_known status and message count.
// Channel keys are server-side config; key material is never exposed via the API.
func GetChannel(w http.ResponseWriter, r *http.Request) {
// channelHash := chi.URLParam(r, "channelHash")
// TODO: fetch channel, write JSON response.
w.WriteHeader(http.StatusNotImplemented)
}
// ListChannelMessages handles GET /api/v1/channels/{channelHash}/messages
//
// Query params (all optional):
// since=<epoch ms>
// limit=50
// cursor=<opaque>
//
// Returns paginated decrypted channel messages. Messages where key_known=false
// will have content=null.
func ListChannelMessages(w http.ResponseWriter, r *http.Request) {
// channelHash := chi.URLParam(r, "channelHash")
// TODO: fetch channel_messages, paginate, write JSON response.
w.WriteHeader(http.StatusNotImplemented)
}
+57 -5
View File
@@ -1,22 +1,58 @@
// Package api defines the response types and read interface for the Tower REST API.
package api
import (
"context"
"time"
)
// ChannelMessage represents a single decrypted channel message.
// Only messages for channels with a known key are stored and returned.
type ChannelMessage struct {
ID int64 `json:"id"`
PacketHash string `json:"packetHash"` // hex-encoded packet hash for correlation with packet events
ChannelHash string `json:"channelHash"` // hex-encoded single-byte channel hash
SenderName string `json:"senderName"` // display name from the decrypted payload
Content string `json:"content"` // decrypted message text
SentAt string `json:"sentAt"` // RFC3339 timestamp from the packet payload
}
// ChannelSummary is the minimal channel representation used in list responses.
type ChannelSummary struct {
ID int `json:"id"`
Name *string `json:"name,omitempty"` // display name, nil if not set
ChannelHash string `json:"channelHash"` // hex-encoded single-byte hash
LastSeen string `json:"lastSeen"` // ISO 8601 timestamp
IsHashtag bool `json:"isHashtag"` // true if derived from a hashtag PSK
KeyKnown bool `json:"keyKnown"` // true if Tower has a decryption key
}
// Channel is the full channel representation including decryption metadata.
// KeyFingerprint is only populated for hashtag channels since their keys are
// publicly derivable from the tag name.
type Channel struct {
ChannelSummary
Hashtag *string `json:"hashtag,omitempty"` // tag name without # prefix
KeyFingerprint *string `json:"keyFingerprint,omitempty"` // first 8 bytes of SHA256(key), hex-encoded
MessageCount int64 `json:"messageCount"`
}
// RegionSummary is the minimal region representation used in list responses.
type RegionSummary struct {
ID int `json:"id"`
Slug string `json:"slug"`
Slug string `json:"slug"` // URL-safe identifier e.g. "western-canada"
Name string `json:"name"`
}
// Region is the full region representation including map display hints and
// the list of IATA codes that are members of this region.
type Region struct {
RegionSummary
Description *string `json:"description,omitempty"`
CenterLat *float64 `json:"centerLat,omitempty"`
CenterLng *float64 `json:"centerLng,omitempty"`
ZoomLevel *int `json:"zoomLevel,omitempty"`
IATAs []string `json:"iatas"`
CenterLat *float64 `json:"centerLat,omitempty"` // map center latitude
CenterLng *float64 `json:"centerLng,omitempty"` // map center longitude
ZoomLevel *int `json:"zoomLevel,omitempty"` // suggested map zoom level
IATAs []string `json:"iatas"` // member IATA codes
}
// IATA represents a known airport/location code used to group observers and packets.
@@ -42,4 +78,20 @@ type Reader interface {
// GetRegion returns full detail for a single region including its associated IATA codes.
// Returns nil, pgx.ErrNoRows if the region is not found.
GetRegion(ctx context.Context, regionID int32) (*Region, error)
// ListChannels returns a summary list of all known channels ordered by last seen.
// Includes both hashtag-derived and explicit key channels.
// Channels with unknown keys are included with KeyKnown=false.
ListChannels(ctx context.Context, limit int32, hash []byte) ([]ChannelSummary, error)
// GetChannel returns full detail for a single channel by its integer ID.
// Returns nil, pgx.ErrNoRows if the channel is not found.
GetChannel(ctx context.Context, channelID int32) (*Channel, error)
// ListChannelMessages returns paginated messages for a channel identified by its integer ID.
// Used by the /channels/{id}/messages endpoint.
// Pass a zero time.Time for since to return all messages up to limit.
ListChannelMessages(ctx context.Context, channelID int32, since time.Time, limit int32) ([]ChannelMessage, error)
// ListChannelMessagesByHash returns paginated messages for all channels matching the given hash.
// Used by the /messages?hash= endpoint. May return messages from multiple channels
// if the hash collides across different keys.
// Pass a zero time.Time for since to return all messages up to limit.
ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32) ([]ChannelMessage, error)
}
+1 -1
View File
@@ -50,7 +50,7 @@ func New(h *hub.Hub, reader api.Reader) http.Handler {
r.Mount("/packets", handlers.PacketsRouter())
r.Mount("/nodes", handlers.NodesRouter())
r.Mount("/observers", handlers.ObserversRouter())
r.Mount("/channels", handlers.ChannelsRouter())
r.Mount("/channels", handlers.ChannelsRouter(reader))
r.Mount("/iatas", handlers.IATAsRouter(reader))
r.Mount("/regions", handlers.RegionsRouter(reader))
r.Mount("/stats", handlers.StatsRouter())