564 lines
18 KiB
Go
564 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/agnivade/levenshtein"
|
|
"github.com/slack-go/slack"
|
|
)
|
|
|
|
// 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 {
|
|
log.Fatalln(err)
|
|
}
|
|
// For each person, parse data and save to database.
|
|
for _, data := range allPeople {
|
|
// Parse the ID and attributes.
|
|
id := data.GetUint64("id")
|
|
attributes := data.GetDict("attributes")
|
|
|
|
// Check if this person is already in our database.
|
|
var p People
|
|
app.db.Where("id = ?", id).First(&p)
|
|
|
|
// Update all fields with new data.
|
|
p.UpdatedAt = attributes.GetDate("updated_at")
|
|
p.ArchivedAt = attributes.GetDate("archived_at")
|
|
p.Birthdate = attributes.GetDate("birthdate")
|
|
p.Anniversary = attributes.GetDate("anniversary")
|
|
p.Status = attributes.GetString("status")
|
|
p.Permissions = attributes.GetString("permissions")
|
|
p.FirstName = attributes.GetString("first_name")
|
|
p.LastName = attributes.GetString("last_name")
|
|
p.FacebookID = attributes.GetUint64("facebook_id")
|
|
|
|
// If the person wasn't in the database, create it.
|
|
if p.ID == 0 {
|
|
p.ID = id
|
|
p.CreatedAt = attributes.GetDate("created_at")
|
|
app.db.Create(&p)
|
|
} else {
|
|
// If th e person was in the database, update it.
|
|
app.db.Save(&p)
|
|
}
|
|
}
|
|
|
|
// Get service types.
|
|
allServiceTypes, err := PCGetAll("/services/v2/service_types")
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
// Keep track of service type IDs incase no filter is supplied.
|
|
var allServiceTypeIDs []uint64
|
|
// For each service type, parse data and save to database.
|
|
for _, data := range allServiceTypes {
|
|
// Get the service ID and attributes.
|
|
id := data.GetUint64("id")
|
|
allServiceTypeIDs = append(allServiceTypeIDs, id)
|
|
attributes := data.GetDict("attributes")
|
|
|
|
// Check if service type was already in database.
|
|
var s ServiceTypes
|
|
app.db.Where("id = ?", id).First(&s)
|
|
|
|
// Update fields with new data.
|
|
s.UpdatedAt = attributes.GetDate("updated_at")
|
|
s.ArchivedAt = attributes.GetDate("archived_at")
|
|
s.DeletedAt = attributes.GetDate("deleted_at")
|
|
s.Name = attributes.GetString("name")
|
|
|
|
// If service type wasn't already existing, create it.
|
|
if s.ID == 0 {
|
|
s.ID = id
|
|
s.CreatedAt = attributes.GetDate("created_at")
|
|
app.db.Create(&s)
|
|
} else {
|
|
// Save if already existing/
|
|
app.db.Save(&s)
|
|
}
|
|
}
|
|
|
|
// Get service type filter from the config.
|
|
servicesTypesToPull := app.config.PlanningCenter.ServiceTypeIDs
|
|
// If no filter, use the found service types above.
|
|
if len(servicesTypesToPull) == 0 {
|
|
servicesTypesToPull = allServiceTypeIDs
|
|
}
|
|
|
|
// For each service type, pull plans and plan info.
|
|
for _, serviceTypeID := range servicesTypesToPull {
|
|
// 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)
|
|
}
|
|
// For each plan, update data in database and pull other plan releated items for updates.
|
|
for _, data := range allPlans {
|
|
// Get the plan ID and attributes.
|
|
planID := data.GetUint64("id")
|
|
attributes := data.GetDict("attributes")
|
|
|
|
// Check if plan was already in the database.
|
|
var p Plans
|
|
app.db.Where("id = ?", planID).First(&p)
|
|
|
|
// Update with new data.
|
|
p.UpdatedAt = attributes.GetDate("updated_at")
|
|
p.SeriesTitle = attributes.GetString("series_title")
|
|
p.Title = attributes.GetString("title")
|
|
p.FirstTimeAt = attributes.GetDate("sort_date")
|
|
p.LastTimeAt = attributes.GetDate("last_time_at")
|
|
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
|
|
p.CreatedAt = attributes.GetDate("created_at")
|
|
p.ServiceType = serviceTypeID
|
|
app.db.Create(&p)
|
|
} else {
|
|
// Save plan if already existing.
|
|
app.db.Save(&p)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
// With each time, save it to the database.
|
|
for _, data := range allPlanTimes {
|
|
// Get the plan time ID and attributes.
|
|
id := data.GetUint64("id")
|
|
attributes := data.GetDict("attributes")
|
|
|
|
// Get from database if already existing.
|
|
var p PlanTimes
|
|
app.db.Where("id = ?", id).First(&p)
|
|
|
|
// Update data.
|
|
p.UpdatedAt = attributes.GetDate("updated_at")
|
|
p.Name = attributes.GetString("name")
|
|
p.TimeType = attributes.GetString("time_type")
|
|
p.StartsAt = attributes.GetDate("starts_at")
|
|
p.EndsAt = attributes.GetDate("ends_at")
|
|
p.LiveStartsAt = attributes.GetDate("live_starts_at")
|
|
p.LiveEndsAt = attributes.GetDate("live_ends_at")
|
|
|
|
// If not already existing, create it.
|
|
if p.ID == 0 {
|
|
p.ID = id
|
|
p.CreatedAt = attributes.GetDate("created_at")
|
|
p.Plan = planID
|
|
app.db.Create(&p)
|
|
} else {
|
|
// If already existing, save it.
|
|
app.db.Save(&p)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
// With each member, update the database.
|
|
for _, data := range allTeamMembers {
|
|
// Get the member ID and attributes.
|
|
id := data.GetUint64("id")
|
|
attributes := data.GetDict("attributes")
|
|
|
|
// Get person data from the database.
|
|
var p PlanPeople
|
|
app.db.Where("id = ?", id).First(&p)
|
|
|
|
// Update data.
|
|
p.UpdatedAt = attributes.GetDate("updated_at")
|
|
p.Status = attributes.GetString("status")
|
|
p.TeamPositionName = attributes.GetString("team_position_name")
|
|
|
|
// If person wasn't existing, create them.
|
|
if p.ID == 0 {
|
|
p.ID = id
|
|
p.CreatedAt = attributes.GetDate("created_at")
|
|
p.Person = data.GetDict("relationships").GetDict("person").GetDict("data").GetUint64("id")
|
|
p.Plan = planID
|
|
app.db.Create(&p)
|
|
} else {
|
|
// Otherwise save new info.
|
|
app.db.Save(&p)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update slack information.
|
|
func UpdateSlackData() {
|
|
// Get all users from Slack.
|
|
users, err := app.slack.GetUsers()
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
// If no users returned, error as we should have some...
|
|
if len(users) == 0 {
|
|
log.Fatalln("No users found in Slack.")
|
|
}
|
|
// With each user, update the database.
|
|
for _, user := range users {
|
|
// Check if user already is in database.
|
|
var u SlackUsers
|
|
app.db.Where("id = ?", user.ID).First(&u)
|
|
|
|
// Update data.
|
|
u.Name = user.Name
|
|
u.RealName = user.RealName
|
|
u.FirstName = user.Profile.FirstName
|
|
u.LastName = user.Profile.LastName
|
|
u.Email = user.Profile.Email
|
|
u.Phone = user.Profile.Phone
|
|
u.Deleted = user.Deleted
|
|
u.IsBot = user.IsBot
|
|
u.IsAdmin = user.IsAdmin
|
|
u.IsOwner = user.IsOwner
|
|
u.IsPrimaryOwner = user.IsPrimaryOwner
|
|
u.IsRestricted = user.IsRestricted
|
|
u.IsUltraRestricted = user.IsUltraRestricted
|
|
u.IsStranger = user.IsStranger
|
|
u.IsAppUser = user.IsAppUser
|
|
u.IsInvitedUser = user.IsInvitedUser
|
|
u.Updated = user.Updated.Time()
|
|
|
|
// Try and find a match for this Slack user to the Planning Center people.
|
|
var people []People
|
|
// Get all people from Planning Center.
|
|
app.db.Find(&people)
|
|
if len(people) != 0 {
|
|
// For each person, compute how close of a match they are to the Slack user.
|
|
for i, person := range people {
|
|
distance := levenshtein.ComputeDistance(u.Name, person.FirstName+" "+person.LastName)
|
|
newDistance := levenshtein.ComputeDistance(u.RealName, person.FirstName+" "+person.LastName)
|
|
// The lowest score of the first+lastname match is used.
|
|
if newDistance < distance {
|
|
distance = newDistance
|
|
}
|
|
// Compute a score of first+last name.
|
|
newDistance = levenshtein.ComputeDistance(u.FirstName, person.FirstName)
|
|
newDistance += levenshtein.ComputeDistance(u.LastName, person.LastName)
|
|
// If this score is lower than the last score, return it.
|
|
if newDistance < distance {
|
|
distance = newDistance
|
|
}
|
|
// Update the distance on the user for sorting.
|
|
people[i].Distance = uint64(distance)
|
|
}
|
|
|
|
// Sort all Planning Center people by the score computed.
|
|
sort.Slice(people, func(i, j int) bool {
|
|
return people[i].Distance < people[j].Distance
|
|
})
|
|
|
|
// Debug output for comparing scores.
|
|
// for _, person := range people {
|
|
// fmt.Printf("%d %s (%s) %s\n", person.Distance, u.Name, u.RealName, person.FirstName+" "+person.LastName)
|
|
// }
|
|
|
|
// Set the planning center ID to nothing at first.
|
|
u.PCID = 0
|
|
// If score of the first person is less than 7,
|
|
// consider them a match and assign thier ID to the slack user.
|
|
if people[0].Distance < 7 {
|
|
u.PCID = people[0].ID
|
|
}
|
|
}
|
|
|
|
// If not already existing in the database, create them.
|
|
if u.ID == "" {
|
|
u.ID = user.ID
|
|
app.db.Create(&u)
|
|
} else {
|
|
// if already existing, update the user.
|
|
app.db.Save(&u)
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
|
|
Delay on channel descript/topic may not be long enough.
|
|
|
|
*/
|
|
|
|
// Create slack channels for upcoming services.
|
|
func CreateSlackChannels() {
|
|
// 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)
|
|
|
|
// Get plan times that match.
|
|
var planTimes []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 len(planTimes) == 0 {
|
|
log.Fatalln("No services found for this time frame.")
|
|
}
|
|
|
|
// With each plan time found, create a slack channel.
|
|
for _, planTime := range planTimes {
|
|
// Get the plan associated with the plan time.
|
|
var plan Plans
|
|
app.db.Where("id = ?", planTime.Plan).First(&plan)
|
|
if plan.ID == 0 {
|
|
log.Println("Unable to find plan:", planTime.Plan)
|
|
continue
|
|
}
|
|
|
|
// Get the service type associated with the plan.
|
|
var serviceType ServiceTypes
|
|
app.db.Where("id = ?", plan.ServiceType).First(&serviceType)
|
|
if serviceType.ID == 0 {
|
|
log.Println("Unable to find service type:", planTime.Plan)
|
|
continue
|
|
}
|
|
|
|
// Find people assigned to the plan.
|
|
var peopleOnPlan []PlanPeople
|
|
app.db.Where("plan = ?", plan.ID).Find(&peopleOnPlan)
|
|
if len(peopleOnPlan) == 0 {
|
|
log.Println("No people assigned to plan:", planTime.Plan)
|
|
continue
|
|
}
|
|
|
|
// Check if a channel was already created for this plan.
|
|
var channel SlackChannels
|
|
app.db.Where("pc_plan = ?", plan.ID).First(&channel)
|
|
|
|
// Set the topic/description based on servie type, and title/series title.
|
|
topic := serviceType.Name
|
|
if plan.SeriesTitle == "" && plan.Title != "" {
|
|
topic = topic + " - " + plan.Title
|
|
} else if plan.SeriesTitle != "" && plan.Title != "" {
|
|
topic = topic + " - " + plan.SeriesTitle + " (" + plan.Title + ")"
|
|
} else if plan.SeriesTitle != "" {
|
|
topic = topic + " - " + plan.SeriesTitle
|
|
}
|
|
|
|
// If the channel already exists, we do not need to create it...
|
|
// However, we should check if the description is changed
|
|
// and we should check if people were added.
|
|
if channel.ID != "" {
|
|
if channel.Description != topic {
|
|
app.slack.SetTopicOfConversation(channel.ID, topic)
|
|
app.slack.SetPurposeOfConversation(channel.ID, topic)
|
|
channel.Description = topic
|
|
app.db.Save(&channel)
|
|
}
|
|
} else {
|
|
// If the channel is being created, set the name to the starts at date.
|
|
channel.Name = planTime.StartsAt.Format("2006-01-02")
|
|
// Its possible that a duplicate channel already exists, if so we should append
|
|
// a channel number. Duplicate channels typically happen if multiple plans
|
|
// exists on the same day.
|
|
startingID := 1
|
|
for {
|
|
var duplicateChannel SlackChannels
|
|
app.db.Where("name = ?", channel.Name).First(&duplicateChannel)
|
|
if duplicateChannel.ID == "" {
|
|
break
|
|
}
|
|
startingID++
|
|
channel.Name = fmt.Sprintf("%s_%d", planTime.StartsAt.Format("2006-01-02"), startingID)
|
|
}
|
|
|
|
// Create the channel.
|
|
channelInfo := slack.CreateConversationParams{
|
|
ChannelName: channel.Name,
|
|
IsPrivate: true,
|
|
}
|
|
log.Println("Creating channel:", channel.Name)
|
|
schan, err := app.slack.CreateConversation(channelInfo)
|
|
if err != nil {
|
|
log.Fatalln("Failed to create channel:", err)
|
|
}
|
|
|
|
// If topic is defined, set the topic and purpose.
|
|
if 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(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.
|
|
channel.ID = schan.ID
|
|
channel.PCPlan = planTime.Plan
|
|
channel.StartsAt = planTime.StartsAt
|
|
channel.EndsAt = planTime.EndsAt
|
|
channel.Description = topic
|
|
app.db.Create(&channel)
|
|
}
|
|
|
|
// Get the previous users that were invited to the channel.
|
|
invited := strings.Split(channel.UsersInvited, ",")
|
|
// If nothing is previous, reset the slice to nil.
|
|
if len(invited) == 1 && invited[0] == "" {
|
|
invited = nil
|
|
}
|
|
|
|
// 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.
|
|
var slackUser SlackUsers
|
|
app.db.Where("pc_id = ?", personOnPlan.Person).First(&slackUser)
|
|
if slackUser.ID == "" {
|
|
continue
|
|
}
|
|
|
|
// Check if they were already invited.
|
|
alreadyInvited := false
|
|
for _, uid := range invited {
|
|
if uid == slackUser.ID {
|
|
alreadyInvited = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Make sure they were not already added to the list of users.
|
|
// A person can be assigned to multiple teams on a plan.
|
|
for _, uid := range usersToInvite {
|
|
if uid == slackUser.ID {
|
|
alreadyInvited = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// If not already invited, add to the list of users to invite.
|
|
if !alreadyInvited {
|
|
usersToInvite = append(usersToInvite, slackUser.ID)
|
|
}
|
|
}
|
|
|
|
// If there are users to invite, invite them.
|
|
if len(usersToInvite) != 0 {
|
|
// Update the invited users list.
|
|
invited = append(invited, usersToInvite...)
|
|
channel.UsersInvited = strings.Join(invited, ",")
|
|
// Invite the users.
|
|
_, err := app.slack.InviteUsersToConversation(channel.ID, usersToInvite...)
|
|
if err != nil {
|
|
log.Println("Failed to invite users to channel:", err)
|
|
}
|
|
// Update the channel on database with the new list of users invited.
|
|
app.db.Save(&channel)
|
|
}
|
|
}
|
|
|
|
// Find old channels to archive. Any channel which start at date is before the start date.
|
|
var channelsToArchive []SlackChannels
|
|
app.db.Where("starts_at < ? AND archived != 1", startDate).Find(&channelsToArchive)
|
|
// Archive channels which are old.
|
|
for _, channel := range channelsToArchive {
|
|
err := app.slack.ArchiveConversation(channel.ID)
|
|
if err != nil {
|
|
log.Println("Error closing old channel:", err)
|
|
}
|
|
// Mark as archived on the database.
|
|
channel.Archived = true
|
|
app.db.Save(&channel)
|
|
}
|
|
}
|