package linkedin import ( "regexp" "strings" ) // MessageType classifies the type of recruiter message. type MessageType int const ( // MsgTypeVague indicates a generic "are you open?" style message. MsgTypeVague MessageType = iota // MsgTypeSpecific indicates a message with role title and details. MsgTypeSpecific // MsgTypeFollowUp indicates a follow-up on a previous conversation. MsgTypeFollowUp // MsgTypeSpam indicates mass outreach or generic spam. MsgTypeSpam ) // ClassifyMessage classifies a recruiter message thread using heuristic patterns. // No LLM call — purely pattern-based for speed. func ClassifyMessage(thread Thread) MessageType { if len(thread.Messages) == 0 { return MsgTypeSpam } lastMsg := thread.Messages[len(thread.Messages)-1] text := strings.ToLower(lastMsg.Text) // Check for follow-up pattern if thread has multiple messages if len(thread.Messages) > 1 && isFollowUpMessage(text) { return MsgTypeFollowUp } // Check for spam indicators if isSpamMessage(text) { return MsgTypeSpam } // Check for specific role details (title + salary/location) if hasJobTitle(text) && (hasSalaryKeywords(text) || hasLocationKeywords(text)) { return MsgTypeSpecific } // Check for vague "open to opportunities" pattern if isVagueMessage(text) { return MsgTypeVague } // Default: if we have a job title but no salary/location, still specific if hasJobTitle(text) { return MsgTypeSpecific } // Default to vague if none of the above return MsgTypeVague } // MissingFields returns a list of standard fields that are absent from the thread. // Checks for: title, company, compensation, remote, location. func MissingFields(thread Thread) []string { combinedText := "" for _, msg := range thread.Messages { combinedText += " " + msg.Text + " " } combinedText = strings.ToLower(combinedText) var missing []string if !hasJobTitle(combinedText) { missing = append(missing, "title") } if !hasCompanyName(combinedText) { missing = append(missing, "company") } if !hasSalaryKeywords(combinedText) { missing = append(missing, "compensation") } if !hasRemotePolicy(combinedText) { missing = append(missing, "remote") } if !hasLocationKeywords(combinedText) { missing = append(missing, "location") } return missing } // DraftInfoRequest creates a polite reply requesting missing information. func DraftInfoRequest(thread Thread, missingFields []string) string { senderFirstName := extractFirstName(thread.Sender) draft := "Hi " + senderFirstName + ",\n\n" draft += "Thanks for reaching out! I'm selectively open to the right opportunity. " draft += "Could you share a few more details?\n\n" for _, field := range missingFields { switch field { case "title": draft += "- What's the role title and team?\n" case "company": draft += "- Which company is this for?\n" case "compensation": draft += "- What's the compensation range?\n" case "remote": draft += "- Is the role remote, hybrid, or on-site?\n" case "location": draft += "- Where is the role based?\n" } } draft += "\nLooking forward to hearing more.\n" return draft } // isVagueMessage checks for vague recruiter phrases without job title. func isVagueMessage(text string) bool { vaguePatterns := []string{ "open to opportunities", "open to the right role", "are you open", "interested in talking", "i came across your profile", "i saw your profile", "reach out", "connect with you", "let's connect", "would love to chat", "great profile", "impressed with your profile", } for _, pattern := range vaguePatterns { if strings.Contains(text, pattern) { return true } } return false } // isFollowUpMessage checks for follow-up indicators. func isFollowUpMessage(text string) bool { followUpPatterns := []string{ "following up", "just checking in", "any update", "any thoughts", "did you have a chance", "circle back", "wanted to follow up", } for _, pattern := range followUpPatterns { if strings.Contains(text, pattern) { return true } } return false } // isSpamMessage checks for spam indicators. func isSpamMessage(text string) bool { // Very short message is often spam if len(strings.Fields(text)) < 5 { return true } spamPatterns := []string{ "i came across your profile", "i saw your profile", } for _, pattern := range spamPatterns { if strings.Contains(text, pattern) { // Check if there's actual job detail after the pattern if !hasJobTitle(text) && !hasSalaryKeywords(text) { return true } } } return false } // hasJobTitle checks for standard job title patterns. func hasJobTitle(text string) bool { // Match patterns like "Senior Engineer", "Lead Developer", "Principal Manager" titlePattern := regexp.MustCompile( `(?i)\b(senior|staff|principal|lead|junior|mid-level|entry-level|manager|director|head|vp|vice president)\s+(engineer|developer|manager|analyst|architect|lead|coordinator|specialist|consultant|scientist|designer)\b`, ) return titlePattern.MatchString(text) } // hasSalaryKeywords checks for compensation-related keywords. func hasSalaryKeywords(text string) bool { salaryKeywords := []string{ "salary", "compensation", "comp", "/year", "k/year", "/annually", "annual", "signing bonus", "equity", "stock options", "benefits", } for _, keyword := range salaryKeywords { if strings.Contains(text, keyword) { return true } } // Check for dollar amounts: $XXXk, $XXX,XXX, etc. dollarPattern := regexp.MustCompile(`\$[\d,]+`) return dollarPattern.MatchString(text) } // hasRemotePolicy checks for work location policy keywords. func hasRemotePolicy(text string) bool { remoteKeywords := []string{ "remote", "hybrid", "on-site", "on-site", "in-office", "office", "wfh", "work from home", "distributed", } for _, keyword := range remoteKeywords { if strings.Contains(text, keyword) { return true } } return false } // hasLocationKeywords checks for geographic location mentions. func hasLocationKeywords(text string) bool { // Simple heuristic: look for US state abbreviations or common city patterns statePattern := regexp.MustCompile( `(?i)\b(ca|ny|tx|fl|il|pa|oh|ga|nc|mi|nj|va|wa|az|ma|tn|in|md|mo|wi|co|mn|sc|al|la|ky|or|ok|ct|ut|ia|nv|ar|ms|kansas|new york|california|texas|florida|illinois|pennsylvania|ohio|georgia|north carolina|michigan|new jersey|virginia|washington|arizona|massachusetts|tennessee|indiana|maryland|missouri|wisconsin|colorado|minnesota|south carolina|alabama|louisiana|kentucky|oregon|oklahoma|connecticut|utah|iowa|nevada|arkansas|mississippi)\b`, ) if statePattern.MatchString(text) { return true } // Check for major cities or country names cityPattern := regexp.MustCompile( `(?i)\b(new york|los angeles|chicago|boston|san francisco|seattle|denver|austin|miami|atlanta|london|toronto|vancouver|sydney|berlin|dubai|singapore|tokyo|india|uk|ireland|canada|australia)\b`, ) return cityPattern.MatchString(text) } // hasCompanyName checks if a company name is mentioned. // Simple heuristic: looks for capitalized words after "at " or "with ". func hasCompanyName(text string) bool { // Check for explicit "at [Company]" pattern atPattern := regexp.MustCompile(`(?i)\bat\s+([A-Z][a-zA-Z0-9\s&-]+)`) if atPattern.MatchString(text) { return true } // Check for "with [Company]" pattern withPattern := regexp.MustCompile(`(?i)\bwith\s+([A-Z][a-zA-Z0-9\s&-]+)`) if withPattern.MatchString(text) { return true } return false } // extractFirstName extracts the first name from a full name string. func extractFirstName(fullName string) string { fullName = strings.TrimSpace(fullName) parts := strings.Fields(fullName) if len(parts) > 0 { return parts[0] } return "there" }