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) } }