feat: Rewrite to use the Diyanet API

master
Abdussamet Kocak 4 weeks ago
parent 43d4560fbb
commit 73316d4e62

@ -0,0 +1,410 @@
openapi: 3.0.3
info:
title: Diyanet Namaz Vaktim API
version: 1.0.0
description: |
OpenAPI spec derived from decompiled Android app usage.
Base URL and endpoints reflect what the client calls.
servers:
- url: https://namazvakti.diyanet.gov.tr/api
security:
- basicAuth: []
tags:
- name: PrayerTimes
- name: Locations
- name: Content
- name: Mosques
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
schemas:
ResultMessage:
type: object
properties:
messageType:
type: integer
nullable: true
messageContent:
type: string
nullable: true
messageCode:
type: integer
nullable: true
PrayerTime:
type: object
properties:
imsak:
type: string
nullable: true
gunes:
type: string
nullable: true
ogle:
type: string
nullable: true
ikindi:
type: string
nullable: true
aksam:
type: string
nullable: true
yatsi:
type: string
nullable: true
gunes_dogus:
type: string
nullable: true
gunes_batis:
type: string
nullable: true
kible_saati:
type: string
nullable: true
hicri_tarih_uzun:
type: string
nullable: true
hicri_tarih_kisa:
type: string
nullable: true
miladi_tarih_uzun:
type: string
nullable: true
miladi_tarih_uzun_Iso8601:
type: string
nullable: true
miladi_tarih_kisa_Iso8601:
type: string
nullable: true
imsak_bg:
type: string
nullable: true
gunes_bg:
type: string
nullable: true
ogle_bg:
type: string
nullable: true
ikindi_bg:
type: string
nullable: true
aksam_bg:
type: string
nullable: true
yatsi_bg:
type: string
nullable: true
ayin_sekli_url:
type: string
nullable: true
MyLocation:
type: object
description: Location object returned under "konum".
properties:
konum_Id:
type: integer
nullable: true
timezone:
type: string
nullable: true
additionalProperties: true
ResultLocation:
type: object
properties:
konum:
$ref: '#/components/schemas/MyLocation'
namazVakti:
type: array
items:
$ref: '#/components/schemas/PrayerTime'
ResultLocationResponse:
type: object
properties:
success:
type: boolean
resultMessage:
$ref: '#/components/schemas/ResultMessage'
resultObject:
$ref: '#/components/schemas/ResultLocation'
serverTime:
type: string
example:
success: true
resultMessage:
messageType: 1
messageContent: Namaz vakitleri başarıyla oluşturuldu
messageCode: 200
resultObject:
konum:
konum_Id: 9340
timezone: Europe/Istanbul
namazVakti:
- imsak: "2026-02-19T06:19:00.0000000+03:00"
gunes: "2026-02-19T07:43:00.0000000+03:00"
ogle: "2026-02-19T13:20:00.0000000+03:00"
ikindi: "2026-02-19T16:18:00.0000000+03:00"
aksam: "2026-02-19T18:47:00.0000000+03:00"
yatsi: "2026-02-19T20:06:00.0000000+03:00"
gunes_dogus: "2026-02-19T07:50:00.0000000+03:00"
gunes_batis: "2026-02-19T18:40:00.0000000+03:00"
kible_saati: "2026-02-19T11:44:00.0000000+03:00"
hicri_tarih_uzun: 1 Ramazan 1447
hicri_tarih_kisa: 1.9.1447
miladi_tarih_uzun: 19 Şubat 2026 Perşembe
miladi_tarih_uzun_Iso8601: "2026-02-19T00:00:00.0000000+03:00"
miladi_tarih_kisa_Iso8601: 19.02.2026
imsak_bg: "https://namazvakti.diyanet.gov.tr/api/images/main_imsak.png?v=6"
gunes_bg: "https://namazvakti.diyanet.gov.tr/api/images/main_gunes.png?v=6"
ogle_bg: "https://namazvakti.diyanet.gov.tr/api/images/main_ogle.png?v=6"
ikindi_bg: "https://namazvakti.diyanet.gov.tr/api/images/main_ikindi.png?v=6"
aksam_bg: "https://namazvakti.diyanet.gov.tr/api/images/main_aksam.png?v=6"
yatsi_bg: "https://namazvakti.diyanet.gov.tr/api/images/main_yatsi.png?v=6"
ayin_sekli_url: "https://namazvakti.diyanet.gov.tr/images/r1.gif"
serverTime: "20/02/2026 - 23:03"
PrayerTimeVersion:
type: object
additionalProperties: true
PrayerTimeVersionResponse:
type: object
properties:
success:
type: boolean
resultMessage:
$ref: '#/components/schemas/ResultMessage'
resultObject:
$ref: '#/components/schemas/PrayerTimeVersion'
serverTime:
type: string
CityInfo:
type: object
additionalProperties: true
CityInfoResponse:
type: object
properties:
success:
type: boolean
resultMessage:
$ref: '#/components/schemas/ResultMessage'
resultObject:
type: array
items:
$ref: '#/components/schemas/CityInfo'
serverTime:
type: string
Mosque:
type: object
additionalProperties: true
MosquesResponse:
type: object
properties:
success:
type: boolean
resultMessage:
$ref: '#/components/schemas/ResultMessage'
resultObject:
type: array
items:
$ref: '#/components/schemas/Mosque'
serverTime:
type: string
SearchLocation:
type: object
properties:
cityID:
type: integer
cityNameTR:
type: string
stateNameTR:
type: string
countryNameTR:
type: string
cityNameEN:
type: string
stateNameEN:
type: string
countryNameEN:
type: string
latitude:
type: number
format: double
longitude:
type: number
format: double
SearchLocationsResults:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/SearchLocation'
SearchLocationsResponse:
type: object
properties:
success:
type: boolean
resultMessage:
$ref: '#/components/schemas/ResultMessage'
resultObject:
$ref: '#/components/schemas/SearchLocationsResults'
serverTime:
type: string
example:
success: true
resultMessage:
messageType: 1
messageContent: İlçeler başarıyla listelendi
messageCode: 200
resultObject:
results:
- cityID: 9340
cityNameTR: İZNİK
stateNameTR: BURSA
countryNameTR: TÜRKİYE
cityNameEN: IZNIK
stateNameEN: BURSA
countryNameEN: TÜRKİYE
latitude: 40.429
longitude: 29.711
serverTime: "20/02/2026 - 23:01"
VersesHadithsPrayersArrays:
type: object
additionalProperties: true
VersesHadithsPrayersResponse:
type: object
properties:
success:
type: boolean
resultMessage:
$ref: '#/components/schemas/ResultMessage'
resultObject:
$ref: '#/components/schemas/VersesHadithsPrayersArrays'
serverTime:
type: string
paths:
/NamazVakti/Aylik:
get:
tags: [PrayerTimes]
summary: Get monthly prayer times
description: |
Returns monthly prayer times for a location.
Provide either ilceId or latitude/longitude.
parameters:
- in: query
name: ilceId
schema:
type: integer
required: false
- in: query
name: latitude
schema:
type: number
format: double
required: false
- in: query
name: longitude
schema:
type: number
format: double
required: false
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ResultLocationResponse'
/Version:
get:
tags: [PrayerTimes]
summary: Get prayer time version
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PrayerTimeVersionResponse'
/Search/GetByName:
get:
tags: [Locations]
summary: Search locations by name
parameters:
- in: query
name: searchText
schema:
type: string
required: true
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SearchLocationsResponse'
/ilce/GetByCoordinat:
get:
tags: [Locations]
summary: Get city/district info by coordinates
parameters:
- in: query
name: latitude
schema:
type: number
format: double
required: true
- in: query
name: longitude
schema:
type: number
format: double
required: true
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/CityInfoResponse'
/Cami/GetByCoordinat:
get:
tags: [Mosques]
summary: Get mosques near coordinates
parameters:
- in: query
name: latitude
schema:
type: number
format: double
required: true
- in: query
name: longitude
schema:
type: number
format: double
required: true
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MosquesResponse'
/AyetDuaHadis/GetAll:
get:
tags: [Content]
summary: Get verses, hadiths, and prayers
parameters:
- in: query
name: dil
schema:
type: string
required: true
description: Language code (e.g., tr)
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/VersesHadithsPrayersResponse'

@ -4,25 +4,19 @@ go 1.25
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/doug-martin/goqu/v9 v9.18.0
github.com/gofiber/fiber/v2 v2.42.0 github.com/gofiber/fiber/v2 v2.42.0
github.com/imroc/req/v3 v3.57.0 github.com/imroc/req/v3 v3.57.0
github.com/rs/zerolog v1.29.0 github.com/rs/zerolog v1.29.0
github.com/samber/lo v1.37.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
modernc.org/sqlite v1.21.0
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/icholy/digest v1.1.0 // indirect github.com/icholy/digest v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
@ -33,7 +27,6 @@ require (
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect github.com/quic-go/quic-go v0.57.1 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/utls v1.8.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
@ -42,21 +35,8 @@ require (
github.com/valyala/fasthttp v1.44.0 // indirect github.com/valyala/fasthttp v1.44.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
) )

@ -1,5 +1,3 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@ -9,34 +7,22 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x0
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY=
github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8= github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8=
github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc= github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI= github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00= github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
@ -44,8 +30,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -55,9 +39,6 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
@ -70,9 +51,6 @@ github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI1
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -81,17 +59,11 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
@ -110,21 +82,15 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017 h1:3Ea9SZLCB0aRIhSEjM+iaGIlzzeDJdpi579El/YIhEE=
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@ -141,8 +107,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -178,44 +142,13 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=

@ -1,9 +1,12 @@
package api package api
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -22,8 +25,13 @@ type httpError struct {
} }
type Services struct { type Services struct {
TimesProvider prayer.TimesProvider Provider DiyanetProvider
LocationTimesProvider prayer.LocationTimesProvider }
type DiyanetProvider interface {
Get(ctx context.Context, locationID string) ([]prayer.Times, error)
GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error)
SearchLocations(ctx context.Context, query string) ([]prayer.Location, error)
} }
func New(services Services) *fiber.App { func New(services Services) *fiber.App {
@ -55,33 +63,77 @@ func New(services Services) *fiber.App {
}) })
app.Get("/api/v1/diyanet/prayertimes", func(ctx *fiber.Ctx) error { app.Get("/api/v1/diyanet/prayertimes", func(ctx *fiber.Ctx) error {
var times []prayer.Times var query struct {
var err error LocationID string `query:"location_id"`
Latitude string `query:"latitude"`
locationID := ctx.Query("location_id") Longitude string `query:"longitude"`
var loc struct { }
Latitude float64 `query:"latitude"` if err := ctx.QueryParser(&query); err != nil {
Longitude float64 `query:"longitude"` return fmt.Errorf("failed to bind prayer times query parameters: %w", errors.Join(fiber.ErrBadRequest, err))
} }
if locationID != "" { locationID := strings.TrimSpace(query.LocationID)
times, err = services.TimesProvider.Get(ctx.Context(), locationID) latitude := strings.TrimSpace(query.Latitude)
} else if err := ctx.QueryParser(&loc); err == nil { longitude := strings.TrimSpace(query.Longitude)
times, err = services.LocationTimesProvider.GetByCoords(ctx.Context(), prayer.Coordinates{
Latitude: loc.Latitude, var (
Longitude: loc.Longitude, times []prayer.Times
err error
)
switch {
case locationID != "":
times, err = services.Provider.Get(ctx.Context(), locationID)
case latitude != "" && longitude != "":
lat, latErr := strconv.ParseFloat(latitude, 64)
if latErr != nil {
return fmt.Errorf("failed to parse latitude query parameter: %w", errors.Join(fiber.ErrBadRequest, latErr))
}
lng, lngErr := strconv.ParseFloat(longitude, 64)
if lngErr != nil {
return fmt.Errorf("failed to parse longitude query parameter: %w", errors.Join(fiber.ErrBadRequest, lngErr))
}
times, err = services.Provider.GetByCoords(ctx.Context(), prayer.Coordinates{
Latitude: lat,
Longitude: lng,
}) })
} else { default:
return fmt.Errorf("%w: missing location id or coordinates", fiber.ErrBadRequest) return fmt.Errorf("failed to validate prayer times query parameters: %w", fiber.ErrBadRequest)
} }
if err != nil { if err != nil {
return err return fmt.Errorf("failed to fetch prayer times: %w", err)
} }
ctx.Response().Header.Add(fiber.HeaderCacheControl, "max-age=86400") ctx.Response().Header.Add(fiber.HeaderCacheControl, "max-age=86400")
return ctx.JSON(fiber.Map{
"prayertimes": times,
})
})
return ctx.JSON(times) app.Get("/api/v1/diyanet/location", func(ctx *fiber.Ctx) error {
var query struct {
Text string `query:"query"`
}
if err := ctx.QueryParser(&query); err != nil {
return fmt.Errorf("failed to bind location query parameters: %w", errors.Join(fiber.ErrBadRequest, err))
}
query.Text = strings.TrimSpace(query.Text)
if query.Text == "" {
return fmt.Errorf("failed to validate location query parameters: %w", fiber.ErrBadRequest)
}
locations, err := services.Provider.SearchLocations(ctx.Context(), query.Text)
if err != nil {
return fmt.Errorf("failed to search locations: %w", err)
}
ctx.Response().Header.Add(fiber.HeaderCacheControl, "max-age=86400")
return ctx.JSON(fiber.Map{
"locations": locations,
})
}) })
app.Use("/", filesystem.New(filesystem.Config{ app.Use("/", filesystem.New(filesystem.Config{

@ -1,38 +0,0 @@
package database
import (
"database/sql"
"fmt"
"net/url"
"github.com/doug-martin/goqu/v9"
goqusqlite3 "github.com/doug-martin/goqu/v9/dialect/sqlite3"
_ "modernc.org/sqlite"
)
func init() {
goqu.RegisterDialect("sqlite3", func() *goqu.SQLDialectOptions {
do := goqusqlite3.DialectOptions()
do.SupportsReturn = true
return do
}())
}
func NewSqliteDB(filename string) (*goqu.Database, error) {
pragmas := url.Values{
"_pragma": {
"journal_mode(WAL)",
"foreign_keys(1)",
"synchronous(NORMAL)",
"busy_timeout(5000)",
},
}.Encode()
dsn := fmt.Sprintf("%s?%s", filename, pragmas)
conn, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
return goqu.New("sqlite3", conn), nil
}

@ -1,11 +1,6 @@
package net package net
import ( import (
"context"
"fmt"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
) )
@ -15,30 +10,3 @@ var ReqClient = req.NewClient().
DisableAutoReadResponse(). DisableAutoReadResponse().
EnableInsecureSkipVerify(). EnableInsecureSkipVerify().
SetUserAgent(userAgent) SetUserAgent(userAgent)
func GetHTML(ctx context.Context, url string) (string, error) {
res, err := ReqClient.R().SetContext(ctx).SetRetryCount(3).Get(url)
if err != nil {
return "", fmt.Errorf("failed to get url: %w", err)
}
html, err := res.ToString()
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
return html, nil
}
func GetParsed(ctx context.Context, url string) (*goquery.Document, error) {
html, err := GetHTML(ctx, url)
if err != nil {
return nil, fmt.Errorf("failed to get html: %w", err)
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, fmt.Errorf("failed to parse html: %w", err)
}
return doc, nil
}
type Fetcher struct {
}

@ -1,28 +1,22 @@
package main package main
import ( import (
"database/sql"
"fmt"
"os" "os"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"prayertimes/internal/api" "prayertimes/internal/api"
"prayertimes/internal/database"
"prayertimes/internal/net" "prayertimes/internal/net"
"prayertimes/pkg/dbtimesprovider"
"prayertimes/pkg/diyanet"
"prayertimes/pkg/diyanetapi" "prayertimes/pkg/diyanetapi"
) )
func main() { func main() {
port := getDefaultEnv("PORT", "8000") port := getDefaultEnv("PORT", "8000")
services, shutdown, err := newServices() services, err := newServices()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed to init services") log.Fatal().Err(err).Msg("failed to init services")
} }
defer shutdown()
app := api.New(services) app := api.New(services)
err = app.Listen(":" + port) err = app.Listen(":" + port)
@ -31,25 +25,12 @@ func main() {
} }
} }
func newServices() (api.Services, func(), error) { func newServices() (api.Services, error) {
diyanetAPIProvider := diyanetapi.New(net.ReqClient) diyanetAPIProvider := diyanetapi.New(net.ReqClient)
diyanetProvider := diyanet.New(net.GetParsed)
db, err := database.NewSqliteDB(getDefaultEnv("DATABASE_URL", "app.sqlite3"))
if err != nil {
return api.Services{}, nil, fmt.Errorf("failed to init db: %w", err)
}
if err := dbtimesprovider.Migrate(db.Db.(*sql.DB)); err != nil {
return api.Services{}, nil, fmt.Errorf("failed to migrate database: %w", err)
}
return api.Services{ return api.Services{
TimesProvider: dbtimesprovider.New(db, diyanetProvider), Provider: diyanetAPIProvider,
LocationTimesProvider: diyanetAPIProvider, }, nil
}, func() {
defer db.Db.(*sql.DB).Close()
}, nil
} }
func getDefaultEnv(name string, defaultValue string) string { func getDefaultEnv(name string, defaultValue string) string {

File diff suppressed because it is too large Load Diff

@ -1,203 +0,0 @@
package dbtimesprovider
import (
"bufio"
"context"
"database/sql"
_ "embed"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/samber/lo"
"prayertimes/pkg/prayer"
)
//go:embed schema.sql
var schema string
//go:embed locations.jsonl
var locationsJSON string
type Provider struct {
db *goqu.Database
provider prayer.TimesProvider
clockFunc func() time.Time
}
func New(db *goqu.Database, provider prayer.TimesProvider) Provider {
return Provider{
provider: provider,
clockFunc: time.Now,
db: db,
}
}
func (p Provider) Name() string {
return "db:" + p.provider.Name()
}
func (p Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
times, err := p.loadTimes(ctx, location)
if err != nil {
return nil, fmt.Errorf("failed to load prayer times from db: %w", err)
}
if len(times) > 0 {
return times, nil
}
times, err = p.provider.Get(ctx, location)
if err != nil {
return nil, fmt.Errorf("failed to get prayer times: %w", err)
}
if len(times) > 0 {
if err := p.saveTimes(ctx, location, times); err != nil {
return nil, fmt.Errorf("failed to save times to db: %w", err)
}
}
return times, nil
}
func Migrate(con *sql.DB) error {
db := goqu.New("sqlite3", con)
if _, err := db.Exec(schema); err != nil {
return fmt.Errorf("failed to migrate: %w", err)
}
count, _ := db.From("locations").Count()
if count > 0 {
return nil
}
type entry struct {
ID int `json:"id" db:"id"`
Country string `json:"country" db:"country"`
Region string `json:"region" db:"region"`
City string `json:"city" db:"city"`
}
s := bufio.NewScanner(strings.NewReader(locationsJSON))
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin tx: %w", err)
}
if err := tx.Wrap(func() error {
for s.Scan() {
var e entry
if err := json.Unmarshal(s.Bytes(), &e); err != nil {
return fmt.Errorf("failed to parse as json: %w", err)
}
q := tx.Insert("locations").
OnConflict(goqu.DoNothing()).
Rows(e)
if _, err := q.Executor().Exec(); err != nil {
return fmt.Errorf("failed to insert location: %w", err)
}
}
return nil
}); err != nil {
return err
}
return nil
}
type prayerTimesRow struct {
ProviderID int64 `db:"provider_id"`
LocationID string `db:"location_id"`
Date string `db:"date"`
Fajr string `db:"fajr"`
Sunrise string `db:"sunrise"`
Dhuhr string `db:"dhuhr"`
Asr string `db:"asr"`
Maghrib string `db:"maghrib"`
Isha string `db:"isha"`
}
func (r prayerTimesRow) toDomain() prayer.Times {
return prayer.Times{
Date: r.Date,
Fajr: r.Fajr,
Sunrise: r.Sunrise,
Dhuhr: r.Dhuhr,
Asr: r.Asr,
Maghrib: r.Maghrib,
Isha: r.Isha,
}
}
func (p Provider) saveTimes(ctx context.Context, locationID string, times []prayer.Times) error {
providerID, err := p.saveProvider(ctx, p.provider.Name())
if err != nil {
return err
}
rows := lo.Map(times, func(item prayer.Times, _ int) prayerTimesRow {
return prayerTimesRow{
ProviderID: providerID,
LocationID: locationID,
Date: item.Date,
Fajr: item.Fajr,
Sunrise: item.Sunrise,
Dhuhr: item.Dhuhr,
Asr: item.Asr,
Maghrib: item.Maghrib,
Isha: item.Isha,
}
})
q := p.db.
Insert("prayer_times").
OnConflict(goqu.DoNothing()).
Rows(rows)
if _, err := q.Executor().ExecContext(ctx); err != nil {
return fmt.Errorf("failed to save times: %w", err)
}
return nil
}
func (p Provider) loadTimes(ctx context.Context, locationID string) ([]prayer.Times, error) {
now := p.clockFunc()
today := now.UTC().Truncate(time.Hour * 24)
q := p.db.
From(goqu.T("prayer_times").As("pt")).
Join(goqu.T("providers").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("pt.provider_id")))).
Where(
goqu.I("p.name").Eq(p.provider.Name()),
goqu.I("pt.location_id").Eq(locationID),
goqu.I("pt.date").Gte(today.Format(time.DateOnly)),
).
Limit(100)
var rows []prayerTimesRow
if err := q.ScanStructsContext(ctx, &rows); err != nil {
return nil, fmt.Errorf("failed to scan times: %w", err)
}
return lo.Map(rows, func(row prayerTimesRow, _ int) prayer.Times {
return row.toDomain()
}), nil
}
func (p Provider) saveProvider(ctx context.Context, name string) (int64, error) {
q := p.db.Insert("providers").
OnConflict(goqu.DoUpdate("name", goqu.Record{"name": name})).
Rows(goqu.Record{"name": name}).
Returning("id")
var id int64
_, err := q.Executor().ScanValContext(ctx, &id)
if err != nil {
return 0, fmt.Errorf("failed to insert provider: %w", err)
}
return id, nil
}

@ -1,122 +0,0 @@
package dbtimesprovider
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
_ "modernc.org/sqlite"
"prayertimes/internal/database"
"prayertimes/pkg/prayer"
)
func testDB(t *testing.T) *goqu.Database {
t.Helper()
db, err := database.NewSqliteDB(":memory:")
require.NoError(t, err)
err = Migrate(db.Db.(*sql.DB))
require.NoError(t, err)
_, err = db.Insert("locations").Rows(goqu.Record{"id": 1}).Executor().Exec()
require.NoError(t, err)
t.Cleanup(func() {
db.Db.(*sql.DB).Close()
})
return db
}
type mockProvider func() ([]prayer.Times, error)
func (m mockProvider) Get(ctx context.Context, location string) ([]prayer.Times, error) { return m() }
func (m mockProvider) Name() string { return "mock" }
func TestProvider_Get(t *testing.T) {
then := time.Date(2023, 3, 5, 0, 0, 0, 0, time.UTC)
tests := []struct {
name string
setupDB func(t *testing.T, db *goqu.Database)
provider prayer.TimesProvider
clock time.Time
assertRes func(t *testing.T, db *goqu.Database, times []prayer.Times, err error)
}{
{
name: "provider succeeds, empty db",
provider: mockProvider(func() ([]prayer.Times, error) {
return []prayer.Times{
{Date: "2023-03-04"},
{Date: "2023-03-05"},
}, nil
}),
clock: then,
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
assert.NoError(t, err)
assert.Len(t, times, 2)
cnt, err := db.From("prayer_times").Count()
assert.NoError(t, err)
assert.Equal(t, int64(2), cnt)
},
},
{
name: "provider fails, empty db",
provider: mockProvider(func() ([]prayer.Times, error) {
return nil, fmt.Errorf("no")
}),
clock: then,
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
assert.Error(t, err)
assert.Empty(t, times)
},
},
{
name: "provider fails, populated db",
setupDB: func(t *testing.T, db *goqu.Database) {
_, err := db.Insert("providers").Rows(goqu.Record{"id": 1, "name": "mock"}).Executor().Exec()
require.NoError(t, err)
_, err = db.Insert("prayer_times").Rows(
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: "2023-03-04", Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: "2023-03-05", Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: "2023-03-06", Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
).Executor().Exec()
require.NoError(t, err)
},
provider: mockProvider(func() ([]prayer.Times, error) {
return nil, fmt.Errorf("no")
}),
clock: then,
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
assert.NoError(t, err)
assert.Len(t, times, 2)
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
db := testDB(t)
p := Provider{
db: db,
provider: tt.provider,
clockFunc: func() time.Time { return tt.clock },
}
if tt.setupDB != nil {
tt.setupDB(t, db)
}
actual, err := p.Get(context.Background(), "1")
tt.assertRes(t, db, actual, err)
})
}
}

@ -1,28 +0,0 @@
CREATE TABLE IF NOT EXISTS locations
(
id text PRIMARY KEY,
country text,
city text,
region text
);
CREATE TABLE IF NOT EXISTS providers
(
id integer PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS prayer_times
(
provider_id integer NOT NULL REFERENCES providers (id),
location_id text NOT NULL REFERENCES locations (id),
date datetime NOT NULL,
fajr text NOT NULL,
sunrise text NOT NULL,
dhuhr text NOT NULL,
asr text NOT NULL,
maghrib text NOT NULL,
isha text NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS prayer_times__provider__location ON prayer_times (provider_id, location_id, date);

@ -1,72 +0,0 @@
package diyanet
import (
"context"
"fmt"
"regexp"
"time"
"github.com/PuerkitoBio/goquery"
"prayertimes/pkg/prayer"
)
type Fetcher interface {
FetchParsed(ctx context.Context, url string) (*goquery.Document, error)
}
type Provider struct {
FetcherFunc func(ctx context.Context, url string) (*goquery.Document, error)
}
func New(fetcherFunc func(ctx context.Context, url string) (*goquery.Document, error)) *Provider {
return &Provider{FetcherFunc: fetcherFunc}
}
var reNumeric = regexp.MustCompile(`\d+`)
func validateLocation(location string) error {
if !reNumeric.MatchString(location) {
return fmt.Errorf("invalid location id")
}
return nil
}
func (d Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
if err := validateLocation(location); err != nil {
return nil, fmt.Errorf("%w: %v", prayer.ErrInvalidLocation, err)
}
u := fmt.Sprintf("https://namazvakitleri.diyanet.gov.tr/en-US/%s", location)
doc, err := d.FetcherFunc(ctx, u)
if err != nil {
return nil, fmt.Errorf("failed to fetch location %q: %w", location, err)
}
var times []prayer.Times
doc.Find("#tab-1 .vakit-table tbody tr").Each(func(_ int, el *goquery.Selection) {
date := el.Find("td:first-of-type").Text()
parsedDate, err := time.Parse("02.01.2006", date)
if err != nil {
return
}
row := prayer.Times{
Date: parsedDate.Format(time.DateOnly),
Fajr: el.Find("td:nth-of-type(3)").Text(),
Sunrise: el.Find("td:nth-of-type(4)").Text(),
Dhuhr: el.Find("td:nth-of-type(5)").Text(),
Asr: el.Find("td:nth-of-type(6)").Text(),
Maghrib: el.Find("td:nth-of-type(7)").Text(),
Isha: el.Find("td:nth-of-type(8)").Text(),
}
times = append(times, row)
})
return times, err
}
func (d Provider) Name() string {
return "diyanetweb"
}

@ -1,125 +0,0 @@
package diyanet
import (
"context"
"strings"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"prayertimes/internal/net"
"prayertimes/pkg/prayer"
)
type mockFetcher string
func (f mockFetcher) Fetch(_ context.Context, _ string) (*goquery.Document, error) {
return goquery.NewDocumentFromReader(strings.NewReader(string(f)))
}
const mockHtml = `
<div id='tab-1'>
<div class='table-responsive'>
<table class='table vakit-table'>
<caption>Monthly Prayer Times for Some Location</caption>
<thead>
<tr>
<th>Gregorian Calendar Date</th>
<th>Hijri Date</th>
<th>Fajr</th>
<th>Sun</th>
<th>Dhuhr</th>
<th>Asr</th>
<th>Maghrib</th>
<th>Isha</th>
</tr>
</thead>
<tbody>
<tr>
<td>04.03.2023</td>
<td>...</td>
<td>05:48</td>
<td>07:11</td>
<td>13:05</td>
<td>16:15</td>
<td>18:49</td>
<td>20:06</td>
</tr>
<tr>
<td>05.03.2023</td>
<td>...</td>
<td>05:46</td>
<td>07:09</td>
<td>13:04</td>
<td>16:15</td>
<td>18:50</td>
<td>20:07</td>
</tr>
</tbody>
</table>
</div>
</div>
`
func TestDiyanet_Get(t *testing.T) {
t.Run("validates location", func(t *testing.T) {
d := Provider{}
_, err := d.Get(context.Background(), " not numeric ")
assert.ErrorIs(t, err, prayer.ErrInvalidLocation)
})
t.Run("extracts prayer times", func(t *testing.T) {
d := Provider{
FetcherFunc: mockFetcher(mockHtml).Fetch,
}
actual, err := d.Get(context.Background(), "1234")
assert.NoError(t, err)
expected := []prayer.Times{
{
Date: "2023-03-04",
Fajr: "05:48",
Sunrise: "07:11",
Dhuhr: "13:05",
Asr: "16:15",
Maghrib: "18:49",
Isha: "20:06",
},
{
Date: "2023-03-05",
Fajr: "05:46",
Sunrise: "07:09",
Dhuhr: "13:04",
Asr: "16:15",
Maghrib: "18:50",
Isha: "20:07",
},
}
assert.Equal(t, expected, actual)
})
t.Run("real endpoint", func(t *testing.T) {
if testing.Short() {
t.Skip()
}
d := Provider{
FetcherFunc: net.GetParsed,
}
times, err := d.Get(context.Background(), "11104")
if err != nil {
t.Skipf("skipping live endpoint test due to upstream/network error: %v", err)
}
if len(times) == 0 {
t.Skip("skipping live endpoint test because upstream returned no times")
}
assert.Greater(t, len(times), 0)
assert.NotZero(t, times[0].Date)
for _, it := range times {
t.Logf("%+v", it)
}
})
}

@ -2,7 +2,9 @@ package diyanetapi
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
@ -21,48 +23,136 @@ func New(c *req.Client) Provider {
} }
func (d Provider) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error) { func (d Provider) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error) {
locationID, err := d.getLocationIDByCoords(ctx, coords)
if err != nil {
return nil, fmt.Errorf("failed to resolve location by coordinates: %w", err)
}
times, err := d.Get(ctx, locationID)
if err != nil {
return nil, fmt.Errorf("failed to get prayer times by coordinates: %w", err)
}
return times, nil
}
func (d Provider) getLocationIDByCoords(ctx context.Context, coords prayer.Coordinates) (string, error) {
res, err := d.http.NewRequest(). res, err := d.http.NewRequest().
SetContext(ctx). SetContext(ctx).
SetQueryParams(map[string]string{ SetQueryParams(map[string]string{
"latitude": fmt.Sprintf("%f", coords.Latitude), "latitude": fmt.Sprintf("%f", coords.Latitude),
"longitude": fmt.Sprintf("%f", coords.Longitude), "longitude": fmt.Sprintf("%f", coords.Longitude),
}). }).
Get("https://namazvakti.diyanet.gov.tr/api/NamazVakti/Aylik") Get("https://namazvakti.diyanet.gov.tr/api/ilce/GetByCoordinat")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get prayer times by coords: %w", err) return "", fmt.Errorf("failed to get location by coords: %w", err)
} }
return d.parseResponse(res) var response struct {
Success bool `json:"success"`
ResultObject []struct {
ID int `json:"cityID"`
} `json:"resultObject"`
}
if err := res.Unmarshal(&response); err != nil {
return "", fmt.Errorf("failed to unmarshal city response: %w", err)
}
if !response.Success {
return "", fmt.Errorf("failed to get location by coordinates from upstream: %w", errors.New(res.String()))
}
if len(response.ResultObject) == 0 {
return "", fmt.Errorf("failed to resolve location by coordinates: %w", errors.New("empty location result"))
}
return fmt.Sprintf("%d", response.ResultObject[0].ID), nil
}
func (d Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
query = strings.TrimSpace(query)
if query == "" {
return []prayer.Location{}, nil
}
res, err := d.http.NewRequest().
SetContext(ctx).
SetQueryParam("searchText", query).
Get("https://namazvakti.diyanet.gov.tr/api/Search/GetByName")
if err != nil {
return nil, fmt.Errorf("failed to search locations: %w", err)
}
var response struct {
Success bool `json:"success"`
ResultObject struct {
Results []struct {
ID int `json:"cityID"`
CityNameTR string `json:"cityNameTR"`
StateNameTR string `json:"stateNameTR"`
CountryNameTR string `json:"countryNameTR"`
CityNameEN string `json:"cityNameEN"`
StateNameEN string `json:"stateNameEN"`
CountryNameEN string `json:"countryNameEN"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"results"`
} `json:"resultObject"`
}
if err := res.Unmarshal(&response); err != nil {
return nil, fmt.Errorf("failed to unmarshal search response: %w", err)
}
if !response.Success {
return nil, fmt.Errorf("failed to search locations in upstream: %w", errors.New(res.String()))
}
locations := make([]prayer.Location, 0, len(response.ResultObject.Results))
for _, it := range response.ResultObject.Results {
locations = append(locations, prayer.Location{
ID: it.ID,
NameTR: formatLocationName(it.CountryNameTR, it.StateNameTR, it.CityNameTR),
NameEN: formatLocationName(it.CountryNameEN, it.StateNameEN, it.CityNameEN),
Latitude: it.Latitude,
Longitude: it.Longitude,
})
}
return locations, nil
}
func formatLocationName(country, state, city string) string {
return fmt.Sprintf("%s / %s / %s", strings.TrimSpace(country), strings.TrimSpace(state), strings.TrimSpace(city))
} }
func (d Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) { func (d Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
res, err := d.http.NewRequest(). res, err := d.http.NewRequest().
SetContext(ctx). SetContext(ctx).
SetQueryParam("ilceId", locationID). SetQueryParam("ilceId", locationID).
Get("https://namazvakti.diyanet.gov.tr/api/NamazVakti/Aylik") Get("https://namazvakti.diyanet.gov.tr/api/NamazVakti/Gunluk")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get prayer times by location id: %w", err) return nil, fmt.Errorf("failed to get prayer times by location id: %w", err)
} }
return d.parseResponse(res) times, err := d.parseResponse(res)
if err != nil {
return nil, fmt.Errorf("failed to parse prayer times response: %w", err)
}
return times, nil
} }
func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) { func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) {
var response struct { var response struct {
Success bool `json:"success"` Success bool `json:"success"`
ResultObject struct { ResultObject struct {
Location struct {
ID int `json:"konum_Id"`
Timezone string `json:"timezone"`
} `json:"konum"`
PrayerTimes []struct { PrayerTimes []struct {
Date time.Time `json:"miladi_tarih_uzun_Iso8601"` Date time.Time `json:"miladi_tarih_uzun_Iso8601"`
Fajr time.Time `json:"imsak"` DateIslamic string `json:"hicri_tarih_uzun"`
Sunrise time.Time `json:"gunes"` Fajr time.Time `json:"imsak"`
Dhuhr time.Time `json:"ogle"` Sunrise time.Time `json:"gunes"`
Asr time.Time `json:"ikindi"` Dhuhr time.Time `json:"ogle"`
Maghrib time.Time `json:"aksam"` Asr time.Time `json:"ikindi"`
Isha time.Time `json:"yatsi"` Sunset time.Time `json:"gunes_batis"`
Maghrib time.Time `json:"aksam"`
Isha time.Time `json:"yatsi"`
} `json:"namazVakti"` } `json:"namazVakti"`
} `json:"resultObject"` } `json:"resultObject"`
} }
@ -70,7 +160,7 @@ func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) {
return nil, fmt.Errorf("failed to unmarshal as json: %w", err) return nil, fmt.Errorf("failed to unmarshal as json: %w", err)
} }
if !response.Success { if !response.Success {
return nil, fmt.Errorf("received error: %s", res.String()) return nil, fmt.Errorf("failed to get prayer times from upstream: %w", errors.New(res.String()))
} }
if len(response.ResultObject.PrayerTimes) == 0 { if len(response.ResultObject.PrayerTimes) == 0 {
@ -80,24 +170,22 @@ func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) {
var times []prayer.Times var times []prayer.Times
today := time.Now().UTC().Truncate(time.Hour * 24) today := time.Now().UTC().Truncate(time.Hour * 24)
for _, pt := range response.ResultObject.PrayerTimes { for _, pt := range response.ResultObject.PrayerTimes {
then := prayer.Date(pt.Date).Time() then := pt.Date.UTC().Truncate(time.Hour * 24)
if then.Before(today) { if then.Before(today) {
continue continue
} }
times = append(times, prayer.Times{ times = append(times, prayer.Times{
Date: pt.Date.Format(time.DateOnly), Date: pt.Date.Format(time.DateOnly),
Fajr: pt.Fajr.Format(time.TimeOnly), DateIslamic: pt.DateIslamic,
Sunrise: pt.Sunrise.Format(time.TimeOnly), Fajr: pt.Fajr.Format("15:04"),
Dhuhr: pt.Dhuhr.Format(time.TimeOnly), Sunrise: pt.Sunrise.Format("15:04"),
Asr: pt.Asr.Format(time.TimeOnly), Dhuhr: pt.Dhuhr.Format("15:04"),
Maghrib: pt.Maghrib.Format(time.TimeOnly), Asr: pt.Asr.Format("15:04"),
Isha: pt.Isha.Format(time.TimeOnly), Sunset: pt.Sunset.Format("15:04"),
Maghrib: pt.Maghrib.Format("15:04"),
Isha: pt.Isha.Format("15:04"),
}) })
} }
return times, nil return times, nil
} }
func (d Provider) Name() string {
return "diyanetapi"
}

@ -19,6 +19,13 @@ func TestDiyanetAPI_GetByCoords(t *testing.T) {
Latitude: 52.5100846, Latitude: 52.5100846,
Longitude: 13.4518284, Longitude: 13.4518284,
}) })
if err != nil {
t.Skipf("skipping live endpoint test due to upstream/network error: %v", err)
}
if len(times) == 0 {
t.Skip("skipping live endpoint test because upstream returned no times")
}
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, times) assert.NotEmpty(t, times)
t.Logf("%#+v", times[0]) t.Logf("%#+v", times[0])
@ -28,6 +35,13 @@ func TestDiyanetAPI_GetByCoords(t *testing.T) {
t.Parallel() t.Parallel()
times, err := p.Get(context.Background(), "11104") times, err := p.Get(context.Background(), "11104")
if err != nil {
t.Skipf("skipping live endpoint test due to upstream/network error: %v", err)
}
if len(times) == 0 {
t.Skip("skipping live endpoint test because upstream returned no times")
}
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, times) assert.NotEmpty(t, times)
for _, time := range times { for _, time := range times {

@ -1,85 +1,32 @@
package prayer package prayer
import ( import (
"context"
"database/sql/driver"
"encoding/json"
"errors" "errors"
"time"
) )
var ErrInvalidLocation = errors.New("invalid location") var ErrInvalidLocation = errors.New("invalid location")
type TimesProvider interface {
Get(ctx context.Context, location string) ([]Times, error)
Name() string
}
type Coordinates struct { type Coordinates struct {
Latitude float64 Latitude float64
Longitude float64 Longitude float64
} }
type LocationTimesProvider interface { type Location struct {
GetByCoords(ctx context.Context, coords Coordinates) ([]Times, error) ID int `json:"id"`
Name() string NameTR string `json:"name_tr"`
} NameEN string `json:"name_en"`
Latitude float64 `json:"latitude"`
type Date time.Time Longitude float64 `json:"longitude"`
func (d Date) String() string {
return time.Time(d).Format(time.DateOnly)
}
func (d *Date) Scan(src any) error {
switch v := src.(type) {
case []byte:
return json.Unmarshal(v, d)
case string:
return json.Unmarshal([]byte(v), d)
case time.Time:
*d = Date(v)
return nil
}
return nil
}
func (d *Date) Value() (driver.Value, error) {
return json.Marshal(d)
}
func (d *Date) UnmarshalJSON(bytes []byte) error {
var t time.Time
if err := json.Unmarshal(bytes, &t); err != nil {
return err
}
_, offset := t.Zone()
t = t.Add(time.Duration(offset * int(time.Second)))
t = t.UTC()
*d = Date(t)
return nil
}
func (d Date) MarshalJSON() ([]byte, error) {
t := time.Time(d)
return json.Marshal(t.Format(time.DateOnly))
}
func (d Date) Time() time.Time {
t := time.Time(d)
_, offset := t.Zone()
t = t.Add(time.Duration(offset * int(time.Second)))
t = t.UTC()
return t
} }
type Times struct { type Times struct {
Date string `json:"date"` Date string `json:"date"`
Fajr string `json:"fajr"` DateIslamic string `json:"date_islamic,omitempty"`
Sunrise string `json:"sunrise"` Fajr string `json:"fajr"`
Dhuhr string `json:"dhuhr"` Sunrise string `json:"sunrise"`
Asr string `json:"asr"` Dhuhr string `json:"dhuhr"`
Maghrib string `json:"maghrib"` Asr string `json:"asr"`
Isha string `json:"isha"` Sunset string `json:"sunset,omitempty"`
Maghrib string `json:"maghrib"`
Isha string `json:"isha"`
} }

@ -1,7 +1,7 @@
const app = () => ({ const app = () => ({
futureTimes: [], futureTimes: [],
locationId: Alpine.$persist('11104'), locationId: Alpine.$persist('11002'),
lastUpdated: Alpine.$persist(null), lastUpdated: Alpine.$persist(null),
userMinutes: 0, userMinutes: 0,
now: new Date(), now: new Date(),
@ -47,9 +47,11 @@ const app = () => ({
const elapsedSeconds = (now - updatedAt) / 1000; const elapsedSeconds = (now - updatedAt) / 1000;
if (this.geolocation !== null) { if (this.geolocation !== null) {
this.futureTimes = await fetchJSON(`/api/v1/diyanet/prayertimes?latitude=${this.geolocation.latitude}&longitude=${this.geolocation.longitude}`); const response = await fetchJSON(`/api/v1/diyanet/prayertimes?latitude=${this.geolocation.latitude}&longitude=${this.geolocation.longitude}`);
this.futureTimes = response.prayertimes ?? [];
} else { } else {
this.futureTimes = await fetchJSON(`/api/v1/diyanet/prayertimes?location_id=${this.locationId}`); const response = await fetchJSON(`/api/v1/diyanet/prayertimes?location_id=${this.locationId}`);
this.futureTimes = response.prayertimes ?? [];
} }
this.lastUpdated = now.toISOString(); this.lastUpdated = now.toISOString();
}, },

Loading…
Cancel
Save