Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42d3251251 | |||
| 6857622c26 | |||
| b6777746e4 | |||
| dd0cab8a9d |
5 changed files with 68 additions and 18 deletions
29
api.go
29
api.go
|
|
@ -2,12 +2,14 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/slack-go/slack"
|
"github.com/slack-go/slack"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Commonly used strings.
|
// Commonly used strings.
|
||||||
|
|
@ -98,13 +100,34 @@ func (s *HTTPServer) RegisterAPIRoutes(r *mux.Router) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
conversation := app.config.Slack.DefaultConversation
|
conversation := app.config.Slack.DefaultConversation
|
||||||
|
|
||||||
// Find plan times that are occuring right now.
|
// Find plan times that are occuring right now. A 60-minute buffer
|
||||||
|
// is applied past ends_at so services that run long still resolve
|
||||||
|
// to their event channel instead of falling back to the default.
|
||||||
|
// Order by starts_at DESC so the most recent active service wins
|
||||||
|
// when a later service's window overlaps an earlier service's buffer.
|
||||||
var planTime PlanTimes
|
var planTime PlanTimes
|
||||||
app.db.Where("time_type='service' AND starts_at < ? AND ends_at > ?", now, now).First(&planTime)
|
err = app.db.Where("time_type='service' AND starts_at < ? AND ends_at > ?", now, now.Add(-60*time.Minute)).Order("starts_at DESC").First(&planTime).Error
|
||||||
|
// A "record not found" simply means no service is occuring right now, in
|
||||||
|
// which case we fall back to the default conversation. Any other error
|
||||||
|
// (e.g. "database is locked") must NOT be swallowed: treating it as "no
|
||||||
|
// service" would silently misroute the message to the default
|
||||||
|
// conversation instead of the event channel. Fail so the caller retries.
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
log.Println("Error looking up plan time:", err)
|
||||||
|
s.APISendGeneralResp(w, APIERR, "Error looking up plan time")
|
||||||
|
return
|
||||||
|
}
|
||||||
if planTime.Plan != 0 {
|
if planTime.Plan != 0 {
|
||||||
// If plan found, check for the slack channel.
|
// If plan found, check for the slack channel.
|
||||||
var channel SlackChannels
|
var channel SlackChannels
|
||||||
app.db.Where("pc_plan = ?", planTime.Plan).First(&channel)
|
err = app.db.Where("pc_plan = ?", planTime.Plan).First(&channel).Error
|
||||||
|
// As above, only "record not found" is a benign result here. On any
|
||||||
|
// other error we must not fall through to the default conversation.
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
log.Println("Error looking up slack channel:", err)
|
||||||
|
s.APISendGeneralResp(w, APIERR, "Error looking up slack channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
if channel.ID != "" {
|
if channel.ID != "" {
|
||||||
// If slack channel found, update the conversation to the channel ID.
|
// If slack channel found, update the conversation to the channel ID.
|
||||||
conversation = channel.ID
|
conversation = channel.ID
|
||||||
|
|
|
||||||
17
database.go
17
database.go
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
|
|
@ -126,7 +127,21 @@ func (a *App) InitDB() {
|
||||||
}
|
}
|
||||||
// Depending on connection configuration, open the database.
|
// Depending on connection configuration, open the database.
|
||||||
if a.config.DB.Type == "sqlite3" {
|
if a.config.DB.Type == "sqlite3" {
|
||||||
a.db, err = gorm.Open(sqlite.Open(a.config.DB.Connection), dbConfig)
|
// Enable WAL journaling and a busy timeout. Without WAL, a single
|
||||||
|
// long-running writer (e.g. the channel-creation/sync routine, which
|
||||||
|
// interleaves slow Slack API calls with its writes) blocks all readers,
|
||||||
|
// causing "database is locked" on concurrent reads such as the
|
||||||
|
// send_message channel lookup. WAL lets reads proceed alongside the
|
||||||
|
// writer, and the busy timeout makes any remaining contention wait
|
||||||
|
// rather than fail immediately. Append as DSN pragmas, preserving any
|
||||||
|
// query string already present in the configured connection.
|
||||||
|
conn := a.config.DB.Connection
|
||||||
|
sep := "?"
|
||||||
|
if strings.Contains(conn, "?") {
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
conn += sep + "_journal_mode=WAL&_busy_timeout=10000"
|
||||||
|
a.db, err = gorm.Open(sqlite.Open(conn), dbConfig)
|
||||||
} else if a.config.DB.Type == "mysql" {
|
} else if a.config.DB.Type == "mysql" {
|
||||||
a.db, err = gorm.Open(mysql.Open(a.config.DB.Connection), dbConfig)
|
a.db, err = gorm.Open(mysql.Open(a.config.DB.Connection), dbConfig)
|
||||||
} else if a.config.DB.Type == "postgres" {
|
} else if a.config.DB.Type == "postgres" {
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -13,7 +13,7 @@ import (
|
||||||
const (
|
const (
|
||||||
serviceName = "service-notifications"
|
serviceName = "service-notifications"
|
||||||
serviceDescription = "Notifications for church services"
|
serviceDescription = "Notifications for church services"
|
||||||
serviceVersion = "0.2"
|
serviceVersion = "0.2.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App is the global application structure for communicating between servers and storing information.
|
// App is the global application structure for communicating between servers and storing information.
|
||||||
|
|
|
||||||
36
update.go
36
update.go
|
|
@ -110,7 +110,8 @@ func UpdatePCData() {
|
||||||
// Get the plans for this service type.
|
// Get the plans for this service type.
|
||||||
allPlans, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans", serviceTypeID))
|
allPlans, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans", serviceTypeID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Println("Error getting plans for service type:", serviceTypeID, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// For each plan, update data in database and pull other plan releated items for updates.
|
// For each plan, update data in database and pull other plan releated items for updates.
|
||||||
for _, data := range allPlans {
|
for _, data := range allPlans {
|
||||||
|
|
@ -152,7 +153,8 @@ func UpdatePCData() {
|
||||||
// Get all times for this plan.
|
// Get all times for this plan.
|
||||||
allPlanTimes, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans/%d/plan_times", serviceTypeID, planID))
|
allPlanTimes, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans/%d/plan_times", serviceTypeID, planID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Println("Error getting plan times for plan:", planID, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// With each time, save it to the database.
|
// With each time, save it to the database.
|
||||||
for _, data := range allPlanTimes {
|
for _, data := range allPlanTimes {
|
||||||
|
|
@ -188,7 +190,8 @@ func UpdatePCData() {
|
||||||
// Get all members of the plan.
|
// Get all members of the plan.
|
||||||
allTeamMembers, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans/%d/team_members", serviceTypeID, planID))
|
allTeamMembers, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans/%d/team_members", serviceTypeID, planID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Println("Error getting team members for plan:", planID, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// With each member, update the database.
|
// With each member, update the database.
|
||||||
for _, data := range allTeamMembers {
|
for _, data := range allTeamMembers {
|
||||||
|
|
@ -226,11 +229,13 @@ func UpdateSlackData() {
|
||||||
// Get all users from Slack.
|
// Get all users from Slack.
|
||||||
users, err := app.slack.GetUsers()
|
users, err := app.slack.GetUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Println("Error getting Slack users:", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// If no users returned, error as we should have some...
|
// If no users returned, error as we should have some...
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
log.Fatalln("No users found in Slack.")
|
log.Println("No users found in Slack.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// With each user, update the database.
|
// With each user, update the database.
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
|
|
@ -354,7 +359,8 @@ func CreateSlackChannels() {
|
||||||
app.db.Where("time_type='service' AND starts_at > ? AND starts_at < ?", startDate, lastDate).Find(&planTimes)
|
app.db.Where("time_type='service' AND starts_at > ? AND starts_at < ?", startDate, lastDate).Find(&planTimes)
|
||||||
// If no plan times matched, exit here.
|
// If no plan times matched, exit here.
|
||||||
if len(planTimes) == 0 {
|
if len(planTimes) == 0 {
|
||||||
log.Fatalln("No services found for this time frame.")
|
log.Println("No services found for this time frame.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// With each plan time found, create a slack channel.
|
// With each plan time found, create a slack channel.
|
||||||
|
|
@ -432,22 +438,28 @@ func CreateSlackChannels() {
|
||||||
log.Println("Creating channel:", channel.Name)
|
log.Println("Creating channel:", channel.Name)
|
||||||
schan, err := app.slack.CreateConversation(channelInfo)
|
schan, err := app.slack.CreateConversation(channelInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to create channel:", err)
|
log.Println("Failed to create channel:", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If topic is defined, set the topic and purpose.
|
// If topic is defined, set the topic and purpose.
|
||||||
if topic != "" {
|
if topic != "" {
|
||||||
// Sleep before, as it takes time for Slack APIs
|
// Keep count of failures so we can try again.
|
||||||
// to recongize the channel was created.
|
failed := 0
|
||||||
time.Sleep(120 * time.Second)
|
_, err = app.slack.SetTopicOfConversation(schan.ID, topic)
|
||||||
_, err = app.slack.SetTopicOfConversation(channel.ID, topic)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
failed++
|
||||||
log.Println("Failed to set topic:", err)
|
log.Println("Failed to set topic:", err)
|
||||||
}
|
}
|
||||||
_, err = app.slack.SetPurposeOfConversation(channel.ID, topic)
|
_, err = app.slack.SetPurposeOfConversation(schan.ID, topic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
failed++
|
||||||
log.Println("Failed to set purpose:", err)
|
log.Println("Failed to set purpose:", err)
|
||||||
}
|
}
|
||||||
|
// If it failed, make topic empty so we can try again next run.
|
||||||
|
if failed != 0 {
|
||||||
|
topic = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the channel to the database.
|
// Save the channel to the database.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue