Compare commits
No commits in common. "master" and "v1.2.0" have entirely different histories.
413
main.go
413
main.go
|
|
@ -18,7 +18,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -124,7 +123,6 @@ func retrieveSubscribers(cfg *Config) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Convert the map keys back to a slice */
|
/* Convert the map keys back to a slice */
|
||||||
/* *** */
|
|
||||||
var subscribers []string
|
var subscribers []string
|
||||||
for email := range emailMap {
|
for email := range emailMap {
|
||||||
subscribers = append(subscribers, email)
|
subscribers = append(subscribers, email)
|
||||||
|
|
@ -353,393 +351,6 @@ func blastMail(cfg *Config, logFile string, trackingFile string, audience []stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendChat(cfg *Config, address string) {
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
ask := func(prompt string, defaultYes bool) bool {
|
|
||||||
var hint string
|
|
||||||
if defaultYes {
|
|
||||||
hint = "(Y/n)"
|
|
||||||
} else {
|
|
||||||
hint = "(y/N)"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s %s: ", prompt, hint)
|
|
||||||
resp, _ := reader.ReadString('\n')
|
|
||||||
resp = strings.TrimSpace(resp)
|
|
||||||
if resp == "" {
|
|
||||||
return defaultYes
|
|
||||||
}
|
|
||||||
r := strings.ToLower(resp)
|
|
||||||
return r == "y" || r == "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 1) Can you vouch? */
|
|
||||||
/* *** */
|
|
||||||
vouch := ask(fmt.Sprintf("Can you vouch for the human identity of %s?", address), true)
|
|
||||||
if !vouch {
|
|
||||||
fmt.Println("You must vouch for the user to invite them. Aborting.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 2) Met in meatspace? */
|
|
||||||
/* *** */
|
|
||||||
var grant_access string
|
|
||||||
met := ask("Have you met this person in meatspace?", true)
|
|
||||||
if met {
|
|
||||||
grant_access = "This user WILL be granted the Verified Human badge."
|
|
||||||
} else {
|
|
||||||
grant_access = "This user will NOT be granted the Verified Human badge."
|
|
||||||
}
|
|
||||||
fmt.Println(grant_access)
|
|
||||||
|
|
||||||
/* 3) Notify Handmade Cities */
|
|
||||||
/* *** */
|
|
||||||
from := cfg.Postmark.SenderEmail
|
|
||||||
if cfg.Postmark.SenderName != "" {
|
|
||||||
from = fmt.Sprintf("%s <%s>", cfg.Postmark.SenderName, cfg.Postmark.SenderEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := PostmarkBatchWithTemplateBody{}
|
|
||||||
body.Messages = append(body.Messages, PostmarkTemplateMessage{
|
|
||||||
From: from,
|
|
||||||
To: "support@handmadecities.com",
|
|
||||||
TemplateId: cfg.Postmark.TemplateId,
|
|
||||||
TemplateModel: PostmarkTemplateModel{
|
|
||||||
Name: cfg.Postmark.SenderName,
|
|
||||||
Email: cfg.Postmark.SenderEmail,
|
|
||||||
Subject: "I'm sending a chat invite",
|
|
||||||
Body: fmt.Sprintf("I vouch for the human identity of %s. %s", address, grant_access),
|
|
||||||
},
|
|
||||||
MessageStream: cfg.Postmark.MessageStream,
|
|
||||||
})
|
|
||||||
|
|
||||||
reqBody, err := json.Marshal(&body)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to prepare request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, cfg.Postmark.ApiUrl, bytes.NewReader(reqBody))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to create request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-Postmark-Server-Token", cfg.Postmark.ServerToken)
|
|
||||||
|
|
||||||
/* Spinner while request runs */
|
|
||||||
/* *** */
|
|
||||||
stop := make(chan struct{})
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
frames := []rune{'|', '/', '-', '\\'}
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stop:
|
|
||||||
close(done)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
fmt.Printf("\r📧 Notifying support@handmadecities.com... %c", frames[i%len(frames)])
|
|
||||||
i++
|
|
||||||
time.Sleep(120 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
failed_notify := "\r\033[31m📧 Failed to notify support@handmadecities.com: %v\033[0m\n"
|
|
||||||
|
|
||||||
res, err := postmarkClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
close(stop)
|
|
||||||
<-done
|
|
||||||
fmt.Printf(failed_notify, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []PostmarkBatchResult
|
|
||||||
resBody, err := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
close(stop)
|
|
||||||
<-done
|
|
||||||
fmt.Printf(failed_notify, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
close(stop)
|
|
||||||
<-done
|
|
||||||
fmt.Printf("\r\033[31m📧 Failed to notify support@handmadecities.com: %d - %s\033[0m\n", res.StatusCode, strings.TrimSpace(string(resBody)))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(resBody, &results)
|
|
||||||
if err != nil {
|
|
||||||
close(stop)
|
|
||||||
<-done
|
|
||||||
fmt.Printf("\r\033[33m📧 Notified support@handmadecities.com but failed to parse Postmark response: %v\033[0m\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
close(stop)
|
|
||||||
<-done
|
|
||||||
fmt.Printf("\r\033[32m📧 Finished notifying support@handmadecities.com\033[0m\n")
|
|
||||||
|
|
||||||
/* 4) Request invite code from Handmade Cities */
|
|
||||||
/* *** */
|
|
||||||
inviteEndpoint := "https://api.handmadecities.com/v1/meetups/chat/invite"
|
|
||||||
hmcPayload := map[string]string{
|
|
||||||
"city": cfg.HMC.City,
|
|
||||||
}
|
|
||||||
hmcReqBody, err := json.Marshal(hmcPayload)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to prepare invite request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hmcReq, err := http.NewRequest(http.MethodPost, inviteEndpoint, bytes.NewReader(hmcReqBody))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to create invite request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
hmcReq.Header.Set("Content-Type", "application/json")
|
|
||||||
hmcReq.Header.Set("Authorization", cfg.HMC.SharedSecret)
|
|
||||||
|
|
||||||
/* Spinner while requesting invite code */
|
|
||||||
/* *** */
|
|
||||||
stopInvite := make(chan struct{})
|
|
||||||
doneInvite := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
frames := []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'}
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopInvite:
|
|
||||||
close(doneInvite)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
fmt.Printf("\r🔑 Requesting invite code... %c", frames[i%len(frames)])
|
|
||||||
i++
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
hmcClient := http.Client{}
|
|
||||||
hmcRes, err := hmcClient.Do(hmcReq)
|
|
||||||
if err != nil {
|
|
||||||
close(stopInvite)
|
|
||||||
<-doneInvite
|
|
||||||
fmt.Printf("\r\033[31m🔑 Failed to retrieve invite code: %v\033[0m\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hmcResBody, _ := io.ReadAll(hmcRes.Body)
|
|
||||||
hmcRes.Body.Close()
|
|
||||||
|
|
||||||
if hmcRes.StatusCode < 200 || hmcRes.StatusCode >= 300 {
|
|
||||||
close(stopInvite)
|
|
||||||
<-doneInvite
|
|
||||||
fmt.Printf("\r\033[31m🔑 Invite request failed: %d - %s\033[0m\n", hmcRes.StatusCode, strings.TrimSpace(string(hmcResBody)))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var inviteResp struct {
|
|
||||||
InviteCode string `json:"invite_code"`
|
|
||||||
InviteLink string `json:"invite_link"`
|
|
||||||
ServerToken string `json:"postmark_server_token"`
|
|
||||||
RegisterTemplateId int `json:"postmark_template_id_register"`
|
|
||||||
JoinTemplateId int `json:"postmark_template_id_join"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(hmcResBody, &inviteResp); err != nil {
|
|
||||||
close(stopInvite)
|
|
||||||
<-doneInvite
|
|
||||||
fmt.Printf("\r\033[33m🔑 Invite request succeeded but failed to parse Postmark response: %v\033[0m\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
close(stopInvite)
|
|
||||||
<-doneInvite
|
|
||||||
fmt.Printf("\r\033[32m🔑 Invite code retrieved and stored\033[0m\n")
|
|
||||||
|
|
||||||
registerBody := map[string]interface{}{
|
|
||||||
"Messages": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"From": from,
|
|
||||||
"To": address,
|
|
||||||
"TemplateId": inviteResp.RegisterTemplateId,
|
|
||||||
"TemplateModel": map[string]string{"token": inviteResp.InviteCode},
|
|
||||||
"MessageStream": "chat",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBody, err = json.Marshal(®isterBody)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to prepare request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = http.NewRequest(http.MethodPost, cfg.Postmark.ApiUrl, bytes.NewReader(reqBody))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to create request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-Postmark-Server-Token", inviteResp.ServerToken)
|
|
||||||
|
|
||||||
/* Spinner while emailing registration */
|
|
||||||
/* *** */
|
|
||||||
stopRegister := make(chan struct{})
|
|
||||||
doneRegister := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
frames := []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'}
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopRegister:
|
|
||||||
close(doneRegister)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
fmt.Printf("\rEmailing account creation... %c", frames[i%len(frames)])
|
|
||||||
i++
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
failedRegister := "\r\033[31m❌ Failed to email account creation: %v\033[0m\n"
|
|
||||||
|
|
||||||
res, err = postmarkClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
close(stopRegister)
|
|
||||||
<-doneRegister
|
|
||||||
fmt.Printf(failedRegister, address, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var registerResults []PostmarkBatchResult
|
|
||||||
resBody, err = io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
close(stopRegister)
|
|
||||||
<-doneRegister
|
|
||||||
fmt.Printf(failedRegister, address, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
close(stopRegister)
|
|
||||||
<-doneRegister
|
|
||||||
fmt.Printf("\r\033[31m❌ Failed to email account creation: %d - %s\033[0m\n", res.StatusCode, strings.TrimSpace(string(resBody)))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(resBody, ®isterResults)
|
|
||||||
if err != nil {
|
|
||||||
close(stopRegister)
|
|
||||||
<-doneRegister
|
|
||||||
fmt.Printf("\r\033[33m⚠️ Emailed account creation but failed to parse Postmark response: %v\033[0m\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
close(stopRegister)
|
|
||||||
<-doneRegister
|
|
||||||
fmt.Printf("\r\033[32m✅ Emailed account creation to %s\033[0m\n", address)
|
|
||||||
|
|
||||||
/* Spinner while emailing server link */
|
|
||||||
/* *** */
|
|
||||||
stopJoin := make(chan struct{})
|
|
||||||
doneJoin := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
frames := []rune{'⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'}
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopJoin:
|
|
||||||
close(doneJoin)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
fmt.Printf("\rEmailing server link... %c", frames[i%len(frames)])
|
|
||||||
i++
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
failedJoin := "\r\033[31m❌ Failed to email server link: %v\033[0m\n"
|
|
||||||
|
|
||||||
joinBody := map[string]interface{}{
|
|
||||||
"Messages": []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"From": from,
|
|
||||||
"To": address,
|
|
||||||
"TemplateId": inviteResp.JoinTemplateId,
|
|
||||||
"TemplateModel": map[string]string{"invite_link": inviteResp.InviteLink},
|
|
||||||
"MessageStream": "chat",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBody, err = json.Marshal(&joinBody)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to prepare request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = http.NewRequest(http.MethodPost, cfg.Postmark.ApiUrl, bytes.NewReader(reqBody))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to create request: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-Postmark-Server-Token", inviteResp.ServerToken)
|
|
||||||
|
|
||||||
res, err = postmarkClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
close(stopJoin)
|
|
||||||
<-doneJoin
|
|
||||||
fmt.Printf(failedJoin, address, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var joinResults []PostmarkBatchResult
|
|
||||||
resBody, err = io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
close(stopJoin)
|
|
||||||
<-doneJoin
|
|
||||||
fmt.Printf(failedJoin, address, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
close(stopJoin)
|
|
||||||
<-doneJoin
|
|
||||||
fmt.Printf("\r\033[31m❌ Failed to email server link: %d - %s\033[0m\n", res.StatusCode, strings.TrimSpace(string(resBody)))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(resBody, &joinResults)
|
|
||||||
if err != nil {
|
|
||||||
close(stopJoin)
|
|
||||||
<-doneJoin
|
|
||||||
fmt.Printf("\r\033[33m⚠️ Emailed server link but failed to parse Postmark response: %v\033[0m\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
close(stopJoin)
|
|
||||||
<-done
|
|
||||||
fmt.Printf("\r\033[32m✅ Emailed server link to %s\033[0m\n", address)
|
|
||||||
}
|
|
||||||
|
|
||||||
var postmarkClient = http.Client{}
|
var postmarkClient = http.Client{}
|
||||||
|
|
||||||
type PostmarkTemplateModel struct {
|
type PostmarkTemplateModel struct {
|
||||||
|
|
@ -834,7 +445,7 @@ func sendMail(cfg *Config, recipients []string, subject, html string) ([]Postmar
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, cfg.Postmark.ApiUrl, bytes.NewReader(reqBody))
|
req, err := http.NewRequest(http.MethodPost, "https://api.postmarkapp.com/email/batchWithTemplates", bytes.NewReader(reqBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// stop spinner before returning
|
// stop spinner before returning
|
||||||
close(stop)
|
close(stop)
|
||||||
|
|
@ -964,13 +575,7 @@ func main() {
|
||||||
* Batch sends your invite using our Email API (Postmark)
|
* Batch sends your invite using our Email API (Postmark)
|
||||||
* Produces .track file that tracks email addresses we attempted to send to
|
* Produces .track file that tracks email addresses we attempted to send to
|
||||||
* Produces .log file with information received back from Postmark
|
* Produces .log file with information received back from Postmark
|
||||||
* You can always blast again: useful when emailing new subscribers. Addresses already in .track file will be skipped
|
* In case of errors, you can always blast again. Addresses already in .track file will be skipped
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
(Optional) Chat server invites
|
|
||||||
* ./meetupinvite2000 chat [email address]
|
|
||||||
* Emails an invitation to join Handmade Cities chat
|
|
||||||
`)
|
`)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1022,25 +627,11 @@ func main() {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
chatCmd := &cobra.Command{
|
|
||||||
Use: "chat [email address]",
|
|
||||||
Short: "Invite a member to the chat server",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) < 1 {
|
|
||||||
cmd.Usage()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
emailAddress := args[0]
|
|
||||||
sendChat(config, emailAddress)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Install commands and serve them to the user */
|
/* Install commands and serve them to the user */
|
||||||
/* *** */
|
/* *** */
|
||||||
cmd.AddCommand(dumpCmd)
|
cmd.AddCommand(dumpCmd)
|
||||||
cmd.AddCommand(testCmd)
|
cmd.AddCommand(testCmd)
|
||||||
cmd.AddCommand(blastCmd)
|
cmd.AddCommand(blastCmd)
|
||||||
cmd.AddCommand(chatCmd)
|
|
||||||
|
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue