1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
|
package database
import (
"context"
"database/sql"
"fmt"
"net/url"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/bcrypt"
"gopkg.in/irc.v4"
)
type MessageTargetLast struct {
Name string
LatestMessage time.Time
}
type MessageOptions struct {
AfterID int64
AfterTime time.Time
BeforeTime time.Time
Limit int
Events bool
Sender string
Text string
TakeLast bool
}
type Database interface {
Close() error
Stats(ctx context.Context) (*DatabaseStats, error)
ListUsers(ctx context.Context) ([]User, error)
GetUser(ctx context.Context, username string) (*User, error)
StoreUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id int64) error
ListInactiveUsernames(ctx context.Context, limit time.Time) ([]string, error)
ListNetworks(ctx context.Context, userID int64) ([]Network, error)
StoreNetwork(ctx context.Context, userID int64, network *Network) error
DeleteNetwork(ctx context.Context, id int64) error
ListChannels(ctx context.Context, networkID int64) ([]Channel, error)
StoreChannel(ctx context.Context, networKID int64, ch *Channel) error
DeleteChannel(ctx context.Context, id int64) error
ListDeliveryReceipts(ctx context.Context, networkID int64) ([]DeliveryReceipt, error)
StoreClientDeliveryReceipts(ctx context.Context, networkID int64, client string, receipts []DeliveryReceipt) error
GetReadReceipt(ctx context.Context, networkID int64, name string) (*ReadReceipt, error)
StoreReadReceipt(ctx context.Context, networkID int64, receipt *ReadReceipt) error
ListWebPushConfigs(ctx context.Context) ([]WebPushConfig, error)
StoreWebPushConfig(ctx context.Context, config *WebPushConfig) error
ListWebPushSubscriptions(ctx context.Context, userID, networkID int64) ([]WebPushSubscription, error)
StoreWebPushSubscription(ctx context.Context, userID, networkID int64, sub *WebPushSubscription) error
DeleteWebPushSubscription(ctx context.Context, id int64) error
GetMessageLastID(ctx context.Context, networkID int64, name string) (int64, error)
GetMessageTarget(ctx context.Context, networkID int64, target string) (*MessageTarget, error)
ListMessageTargets(ctx context.Context, networkID int64) ([]MessageTarget, error)
StoreMessageTarget(ctx context.Context, networkID int64, mt *MessageTarget) error
StoreMessages(ctx context.Context, networkID int64, name string, msgs []*irc.Message) ([]int64, error)
ListMessageLastPerTarget(ctx context.Context, networkID int64, options *MessageOptions) ([]MessageTargetLast, error)
ListMessages(ctx context.Context, networkID int64, name string, options *MessageOptions) ([]*irc.Message, error)
}
type MetricsCollectorDatabase interface {
Database
RegisterMetrics(r prometheus.Registerer) error
}
func Open(driver, source string) (Database, error) {
switch driver {
case "sqlite3":
return OpenSqliteDB(source)
case "postgres":
return OpenPostgresDB(source)
default:
return nil, fmt.Errorf("unsupported database driver: %q", driver)
}
}
type DatabaseStats struct {
Users int64
Networks int64
Channels int64
}
type User struct {
ID int64
Username string
Password string // hashed
Nick string
Realname string
Admin bool
Enabled bool
DownstreamInteractedAt time.Time
MaxNetworks int
}
func NewUser(username string) *User {
return &User{
Username: username,
Enabled: true,
MaxNetworks: -1,
}
}
func (u *User) CheckPassword(password string) (upgraded bool, err error) {
if u.Password == "" {
return false, fmt.Errorf("password auth disabled")
}
err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
if err != nil {
return false, fmt.Errorf("wrong password: %v", err)
}
passCost, err := bcrypt.Cost([]byte(u.Password))
if err != nil {
return false, fmt.Errorf("invalid password cost: %v", err)
}
if passCost < bcrypt.DefaultCost {
return true, u.SetPassword(password)
}
return false, nil
}
func (u *User) SetPassword(password string) error {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %v", err)
}
u.Password = string(hashed)
return nil
}
type SASL struct {
Mechanism string
Plain struct {
Username string
Password string
}
// TLS client certificate authentication.
External struct {
// X.509 certificate in DER form.
CertBlob []byte
// PKCS#8 private key in DER form.
PrivKeyBlob []byte
}
}
type Network struct {
ID int64
Name string
Addr string
Nick string
Username string
Realname string
Pass string
ConnectCommands []string
CertFP string
SASL SASL
AutoAway bool
Enabled bool
}
func NewNetwork(addr string) *Network {
return &Network{
Addr: addr,
AutoAway: true,
Enabled: true,
}
}
func (net *Network) GetName() string {
if net.Name != "" {
return net.Name
}
return net.Addr
}
func (net *Network) URL() (*url.URL, error) {
s := net.Addr
if !strings.Contains(s, "://") {
// This is a raw domain name, make it a URL with the default scheme
s = "ircs://" + s
}
u, err := url.Parse(s)
if err != nil {
return nil, fmt.Errorf("failed to parse upstream server URL: %v", err)
}
switch u.Scheme {
case "irc+unix", "unix":
u.Path = u.Host + u.Path
u.Host = ""
}
return u, nil
}
func GetNick(user *User, net *Network) string {
if net != nil && net.Nick != "" {
return net.Nick
}
if user.Nick != "" {
return user.Nick
}
return user.Username
}
func GetUsername(user *User, net *Network) string {
if net != nil && net.Username != "" {
return net.Username
}
return GetNick(user, net)
}
func GetRealname(user *User, net *Network) string {
if net != nil && net.Realname != "" {
return net.Realname
}
if user.Realname != "" {
return user.Realname
}
return GetNick(user, net)
}
type MessageFilter int
const (
// TODO: use customizable user defaults for FilterDefault
FilterDefault MessageFilter = iota
FilterNone
FilterHighlight
FilterMessage
)
type Channel struct {
ID int64
Name string
Key string
Detached bool
DetachedInternalMsgID string
RelayDetached MessageFilter
ReattachOn MessageFilter
DetachAfter time.Duration
DetachOn MessageFilter
}
type DeliveryReceipt struct {
ID int64
Target string // channel or nick
Client string
InternalMsgID string
}
type ReadReceipt struct {
ID int64
Target string // channel or nick
Timestamp time.Time
}
type WebPushConfig struct {
ID int64
VAPIDKeys struct {
Public, Private string
}
}
type WebPushSubscription struct {
ID int64
Endpoint string
CreatedAt, UpdatedAt time.Time // read-only
Keys struct {
Auth string
P256DH string
VAPID string
}
}
type MessageTarget struct {
ID int64
Target string
Pinned bool
Muted bool
Blocked bool
}
func toNullString(s string) sql.NullString {
return sql.NullString{
String: s,
Valid: s != "",
}
}
func toNullTime(t time.Time) sql.NullTime {
return sql.NullTime{
Time: t,
Valid: !t.IsZero(),
}
}
|