Compare commits

..

4 Commits
v0.1 ... main

Author SHA1 Message Date
6857622c26 Replace fatal errors with non-fatal logging to improve resilience
Version bump to 0.2.1. Changed log.Fatalln calls to log.Println with
continue/return so that individual API errors no longer crash the
entire service.
2026-03-22 10:38:42 -05:00
b6777746e4 Fix topic set after channel creation 2023-10-05 19:32:19 -05:00
dd0cab8a9d Fix readme 2023-10-01 08:00:16 -05:00
2354e60b3d Added support for creating slack channels with reference to a weekday, added support for specifying users to always add to a channel, maybe more? 2023-10-01 07:58:52 -05:00
6 changed files with 115 additions and 22 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
config.yaml
service-notifications
service-notifications.db
service-notifications.db
sync.sh

View File

@ -110,6 +110,7 @@ Get Slack API token by creating an app at https://api.slack.com/apps then go to
Get Planning Center API secrets at https://api.planningcenteronline.com/oauth/applications by creating a personal access token.
You can get a slack user ID by viewing the profile and under the 3 dot menu choose Copy member ID.
```yaml
---
@ -122,6 +123,9 @@ planning_center:
slack:
api_token: SLACK_API_TOKEN
admin_id: SLACK_UID
create_from_weekday: 3
default_conversation: SLACK_UID
sticky_users:
- SLACK_UID
```

2
api.go
View File

@ -96,7 +96,7 @@ func (s *HTTPServer) RegisterAPIRoutes(r *mux.Router) {
// Get current time and default conversation.
now := time.Now().UTC()
conversation := app.config.Slack.AdminID
conversation := app.config.Slack.DefaultConversation
// Find plan times that are occuring right now.
var planTime PlanTimes

View File

@ -35,9 +35,11 @@ type PlanningCenterConfig struct {
// Configurations relating to Slack API/channel creation.
type SlackConfig struct {
CreateFromWeekday int `fig:"create_from_weekday"` // Create ahead from this weekday. -1 value is default and will instead create from the current time of operation.
CreateChannelsAhead time.Duration `fig:"create_channels_ahead"` // Amount of time of future services to create channels head for. Defaults to 8 days head.
APIToken string `fig:"api_token"`
AdminID string `fig:"admin_id"` // Slack user that administers this app.
StickyUsers []string `fig:"sticky_users"` // Users to add to every channel.
DefaultConversation string `fig:"default_conversation"` // Slack user that administers this app.
}
// Configuration Structure.
@ -86,6 +88,7 @@ func (a *App) ReadConfig() {
Connection: "service-notifications.db",
},
Slack: SlackConfig{
CreateFromWeekday: -1,
CreateChannelsAhead: time.Hour * 24 * 8,
},
}

View File

@ -13,7 +13,7 @@ import (
const (
serviceName = "service-notifications"
serviceDescription = "Notifications for church services"
serviceVersion = "0.1"
serviceVersion = "0.2.1"
)
// App is the global application structure for communicating between servers and storing information.

119
update.go
View File

@ -13,6 +13,19 @@ import (
// Update planning center database tables with data from PC API.
func UpdatePCData() {
// We don't need to update the archive if we already have data from the past,
// as such we get the current time and see if we already have an entry in the future.
// Its possible that some people only schedule services once a week, so we check with
// the current date subtracted by 14 days.
var updateFrom time.Time
now := time.Now().UTC()
var futurePlan Plans
app.db.Where("first_time_at >= ?", now.Add(time.Hour*24*14*-1)).Order("first_time_at ASC").First(&futurePlan)
if futurePlan.ID != 0 {
// If a future plan exists, we update from past 30 days.
updateFrom = now.Add(time.Hour * 24 * 30 * -1)
}
// Get all people.
allPeople, err := PCGetAll("/services/v2/people")
if err != nil {
@ -97,7 +110,8 @@ func UpdatePCData() {
// Get the plans for this service type.
allPlans, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans", serviceTypeID))
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 _, data := range allPlans {
@ -118,6 +132,13 @@ func UpdatePCData() {
p.MultiDay = attributes.GetBool("multi_day")
p.Dates = attributes.GetString("dates")
// If either updated at or first time at for the plan is before the update from date,
// we can process the update of data. Otherwise, we ignore this service as we do not care
// about updating historic data. Updating historic data causes more API traffic than needed.
if p.UpdatedAt.Before(updateFrom) && p.FirstTimeAt.Before(updateFrom) {
continue
}
// If plan wasn't already created, create it.
if p.ID == 0 {
p.ID = planID
@ -132,7 +153,8 @@ func UpdatePCData() {
// Get all times for this plan.
allPlanTimes, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans/%d/plan_times", serviceTypeID, planID))
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.
for _, data := range allPlanTimes {
@ -168,7 +190,8 @@ func UpdatePCData() {
// Get all members of the plan.
allTeamMembers, err := PCGetAll(fmt.Sprintf("/services/v2/service_types/%d/plans/%d/team_members", serviceTypeID, planID))
if err != nil {
log.Fatalln(err)
log.Println("Error getting team members for plan:", planID, err)
continue
}
// With each member, update the database.
for _, data := range allTeamMembers {
@ -206,11 +229,13 @@ func UpdateSlackData() {
// Get all users from Slack.
users, err := app.slack.GetUsers()
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 len(users) == 0 {
log.Fatalln("No users found in Slack.")
log.Println("No users found in Slack.")
return
}
// With each user, update the database.
for _, user := range users {
@ -291,13 +316,41 @@ func UpdateSlackData() {
}
}
/*
Delay on channel descript/topic may not be long enough.
*/
// Create slack channels for upcoming services.
func CreateSlackChannels() {
// For now, we're using the start time of now. I want to update this later to allow
// setting a day of the week for slack channels to be created on.
// Doing a day of the week will allow for channels to be created ahead of time, then
// if people are added to the plan later on, they can be added at the next cron run.
startDate := time.Now().UTC()
// Start at now.
now := time.Now().UTC()
startDate := now
// If create from weekday is a valid weekday, attempt to turn back the clock to the
// most recently past weekday. Use that day as the stating point so we do not
// create channels in the future past the date we expect to have channels.
// This is useful if you want to run the cron every day to keep channel title
// and members up to date, but only want so many channels ahead of a certain weekday.
if app.config.Slack.CreateFromWeekday != -1 && app.config.Slack.CreateFromWeekday <= 6 {
// Get the current weekday and set the days to subtract to 0.
thisWeekday := int(now.Weekday())
var daysSub int = 0
// If this weekday is the day we intend to create from, or if the weekday is
// after. We want to just subtract this weekday from create form weekday which
// should get us back to the most recent weekday.
if thisWeekday >= app.config.Slack.CreateFromWeekday {
daysSub = app.config.Slack.CreateFromWeekday - thisWeekday
} else {
// Otherwise, we have started a new week from that weekday and we need to
// add 7 days to the current weekday in our subtraction. This will bring us
// not to the next weekday, but the past weekday.
daysSub = app.config.Slack.CreateFromWeekday - (thisWeekday + 7)
}
// Subtract the number of days calculated to bring us to the weekday to create form.
startDate = now.Add(time.Hour * 24 * time.Duration(daysSub))
}
// Last date is start date plus duration of create channels ahead.
lastDate := startDate.Add(app.config.Slack.CreateChannelsAhead)
@ -306,7 +359,8 @@ func CreateSlackChannels() {
app.db.Where("time_type='service' AND starts_at > ? AND starts_at < ?", startDate, lastDate).Find(&planTimes)
// If no plan times matched, exit here.
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.
@ -384,22 +438,28 @@ func CreateSlackChannels() {
log.Println("Creating channel:", channel.Name)
schan, err := app.slack.CreateConversation(channelInfo)
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 != "" {
// Sleep before, as it takes time for Slack APIs
// to recongize the channel was created.
time.Sleep(10 * time.Second)
_, err = app.slack.SetTopicOfConversation(channel.ID, topic)
// Keep count of failures so we can try again.
failed := 0
_, err = app.slack.SetTopicOfConversation(schan.ID, topic)
if err != nil {
failed++
log.Println("Failed to set topic:", err)
}
_, err = app.slack.SetPurposeOfConversation(channel.ID, topic)
_, err = app.slack.SetPurposeOfConversation(schan.ID, topic)
if err != nil {
failed++
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.
@ -421,6 +481,31 @@ func CreateSlackChannels() {
// Keep a list of users we need to invite as they are new.
var usersToInvite []string
// For each sticky user, invite them.
for _, stickyUser := range app.config.Slack.StickyUsers {
// Check if they were already invited.
alreadyInvited := false
for _, uid := range invited {
if uid == stickyUser {
alreadyInvited = true
break
}
}
// Make sure they were not already added to the list of users.
for _, uid := range usersToInvite {
if uid == stickyUser {
alreadyInvited = true
break
}
}
// If not already invited, add to the list of users to invite.
if !alreadyInvited {
usersToInvite = append(usersToInvite, stickyUser)
}
}
// For each person on the plan, see if we need to invite them.
for _, personOnPlan := range peopleOnPlan {
// Find the slack user for the planning center person.