so much
This commit is contained in:
1 parent
136a1c34b3
commit
cb87af4f3b
108 files changed
+49079
-41
No files matched your search
@@ -0,0 +1,87 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceToken struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type DeviceTokenModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (m DeviceTokenModel) Insert(userID int64, token string) error {
|
||||
query := `
|
||||
INSERT INTO device_tokens (user_id, token)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, userID, token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m DeviceTokenModel) GetForUser(userID int64) ([]DeviceToken, error) {
|
||||
query := `SELECT id, user_id, token, created_at FROM device_tokens WHERE user_id = $1`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tokens []DeviceToken
|
||||
for rows.Next() {
|
||||
var dt DeviceToken
|
||||
if err := rows.Scan(&dt.ID, &dt.UserID, &dt.Token, &dt.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, dt)
|
||||
}
|
||||
return tokens, rows.Err()
|
||||
}
|
||||
|
||||
func (m DeviceTokenModel) GetAllTokens() ([]DeviceToken, error) {
|
||||
query := `SELECT id, user_id, token, created_at FROM device_tokens`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tokens []DeviceToken
|
||||
for rows.Next() {
|
||||
var dt DeviceToken
|
||||
if err := rows.Scan(&dt.ID, &dt.UserID, &dt.Token, &dt.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, dt)
|
||||
}
|
||||
return tokens, rows.Err()
|
||||
}
|
||||
|
||||
func (m DeviceTokenModel) Delete(token string) error {
|
||||
query := `DELETE FROM device_tokens WHERE token = $1`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, token)
|
||||
return err
|
||||
}
|
||||
@@ -20,6 +20,8 @@ type Models struct {
|
||||
BlindSigns BlindSignModel
|
||||
Votes VoteModel
|
||||
Options OptionModel
|
||||
ParlWatcher ParlWatcherModel
|
||||
DeviceTokens DeviceTokenModel
|
||||
}
|
||||
|
||||
func NewModels(db *sql.DB) Models {
|
||||
@@ -33,5 +35,7 @@ func NewModels(db *sql.DB) Models {
|
||||
BlindSigns: BlindSignModel{DB: db},
|
||||
Votes: VoteModel{DB: db},
|
||||
Options: OptionModel{DB: db},
|
||||
ParlWatcher: ParlWatcherModel{DB: db},
|
||||
DeviceTokens: DeviceTokenModel{DB: db},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type ParlWatcherModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// FilterNew returns only the numbers from the input that are not yet recorded.
|
||||
func (m ParlWatcherModel) FilterNew(numbers []string) ([]string, error) {
|
||||
query := `
|
||||
SELECT unnest($1::text[]) AS num
|
||||
EXCEPT
|
||||
SELECT number FROM parl_notified_votes`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query, pq.Array(numbers))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []string
|
||||
for rows.Next() {
|
||||
var n string
|
||||
if err := rows.Scan(&n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, n)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// MarkNotified records the given numbers so they are not notified again.
|
||||
func (m ParlWatcherModel) MarkNotified(numbers []string) error {
|
||||
query := `
|
||||
INSERT INTO parl_notified_votes (number)
|
||||
SELECT unnest($1::text[])
|
||||
ON CONFLICT (number) DO NOTHING`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, pq.Array(numbers))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAllNotifiedNumbers returns every number that has already been notified.
|
||||
// Unused in the hot path but useful for debugging/admin.
|
||||
func (m ParlWatcherModel) GetAllNotifiedNumbers() ([]string, error) {
|
||||
query := `SELECT number FROM parl_notified_votes ORDER BY notified_at DESC`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []string
|
||||
for rows.Next() {
|
||||
var n string
|
||||
if err := rows.Scan(&n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, n)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// Ensure ParlWatcherModel satisfies the interface even if DB is nil (e.g. in tests).
|
||||
var _ = sql.ErrNoRows
|
||||
@@ -392,3 +392,26 @@ WHERE id = $1`
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m UserModel) GetAllActivated() ([]*User, error) {
|
||||
query := `SELECT id, email, name FROM users WHERE activated = true ORDER BY id`
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []*User
|
||||
for rows.Next() {
|
||||
u := &User{}
|
||||
if err := rows.Scan(&u.ID, &u.Email, &u.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
return users, rows.Err()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{{define "subject"}}{{.Count}} neue Parlamentsabstimmung{{if gt .Count 1}}en{{end}}{{end}}
|
||||
|
||||
{{define "plainBody"}}
|
||||
Hallo {{.Name}},
|
||||
|
||||
im Nationalrat wurde{{if gt .Count 1}}n{{end}} {{.Count}} neue Abstimmung{{if gt .Count 1}}en{{end}} durchgeführt:
|
||||
|
||||
{{range .Docs}}
|
||||
• {{.Date}} — {{.Title}} ({{.Number}})
|
||||
{{if .VoteText}}{{.VoteText}}{{end}}
|
||||
{{end}}
|
||||
|
||||
Details unter: https://party.at/parlament
|
||||
|
||||
Die Digitale Partei
|
||||
{{end}}
|
||||
|
||||
{{define "htmlBody"}}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style>
|
||||
body { font-family: sans-serif; font-size: 15px; color: #111; background: #f9f9f9; margin: 0; padding: 0; }
|
||||
.wrap { max-width: 560px; margin: 40px auto; background: #fff; border-radius: 8px; border: 1px solid #e5e7eb; padding: 32px; }
|
||||
h2 { margin: 0 0 20px; font-size: 20px; }
|
||||
.vote { border: 1px solid #e5e7eb; border-radius: 6px; padding: 14px 16px; margin-bottom: 12px; }
|
||||
.vote-title { font-weight: 600; margin-bottom: 4px; }
|
||||
.vote-meta { font-size: 13px; color: #6b7280; margin-bottom: 6px; }
|
||||
.vote-result { font-size: 13px; }
|
||||
.footer { margin-top: 28px; font-size: 13px; color: #6b7280; }
|
||||
a { color: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h2>{{.Count}} neue Parlamentsabstimmung{{if gt .Count 1}}en{{end}}</h2>
|
||||
<p>Hallo {{.Name}},</p>
|
||||
<p>im Nationalrat wurde{{if gt .Count 1}}n{{end}} soeben folgende Abstimmung{{if gt .Count 1}}en{{end}} durchgeführt:</p>
|
||||
|
||||
{{range .Docs}}
|
||||
<div class="vote">
|
||||
<div class="vote-title">{{.Title}}</div>
|
||||
<div class="vote-meta">{{.Date}} · {{.Number}}</div>
|
||||
{{if .VoteText}}<div class="vote-result">{{.VoteText}}</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<p><a href="https://party.at/parlament">Alle Parlamentsabstimmungen ansehen →</a></p>
|
||||
<div class="footer">Die Digitale Partei</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in new issue
Block a user