Compare commits

..

1 commit

Author SHA1 Message Date
2145ce1134 Make a thing worth running (#7)
This is, admittedly, mostly the [twilio-go](https://github.com/twilio/twilio-go) Quickstart, adapted with positional arguments and environment variables for secrets. But it's definitely a start!

Co-authored-by: Dani Sweet <djsweet@users.noreply.github.com>
Reviewed-on: #7
Reviewed-by: maren <git@stillgreenmoss.net>
2024-01-08 00:47:15 +00:00
9 changed files with 157 additions and 1826 deletions

5
.gitignore vendored
View file

@ -1,8 +1,3 @@
.env
.envrc
.DS_Store
# Added by cargo
/target

1604
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +0,0 @@
[package]
name = "smsbb"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aws-config = { version = "1.1.5", features = ["sso"] }
aws-sdk-dynamodb = "1.14.0"
aws-sdk-sns = "1.13.0"
aws-sdk-sqs = "1.13.0"
clap = "4.4.18"
tokio = { version = "1.36.0", features = ["full"] }

View file

@ -1,37 +0,0 @@
# smsbb 👼
✨ DIY sms announce lists for you and yours ✨
## norms for working on this repo
- work in a branch
- branch names should use the format `username/thing-you-are-doing`
- for example `git checkout -b maren/update-readme-3`
- don't worry about tidy commits! just keep your work in a branch
- open a PR and review with someone synchronously before merging to main
## getting started real fast
1. Make sure you have Rust (+ cargo) installed. The preferred way to do this
is with [rustup](https://rustup.rs).
2. Set up [an AWS Account](https://aws.amazon.com). Following the getting
started guide for the [AWS SDK For Rust](https://aws.amazon.com/sdk-for-rust/)
is a good way to get set up. We can confirm that this all works correctly
using Workforce Identity, i.e. establishing a profile in `~/.aws/config`,
setting the `AWS_PROFILE` environment variable appropriately, and using
`aws sso login` to get an access token.
3. With your preferred authentication mechanism set up (see above for
settings for Workforce Identity), run
```
cargo run
```
As of right this second, all the code does is set up DynamoDB tables.
The immediate future work here is:
1. Write code to provision AWS Pinpoint for each of smsbb's text campaigns
2. Figure out how to embed another Rust binary inside the main Rust binary
3. Use the embedded Rust binaries to deploy Lambda functions

10
go.mod Normal file
View file

@ -0,0 +1,10 @@
module git.bunk.computer/bunk/smsbb
go 1.21.4
require github.com/twilio/twilio-go v1.16.0
require (
github.com/golang/mock v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
)

50
go.sum Normal file
View file

@ -0,0 +1,50 @@
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/twilio/twilio-go v1.16.0 h1:NabGDInWQYPFVAz2TPxpjfPFeoDlaB3MIww1PlZU+rM=
github.com/twilio/twilio-go v1.16.0/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

48
readme.md Normal file
View file

@ -0,0 +1,48 @@
# smsbb 👼
✨ DIY sms announce lists for you and yours ✨
## norms for working on this repo
- work in a branch
- branch names should use the format `username/thing-you-are-doing`
- for example `git checkout -b maren/update-readme-3`
- don't worry about tidy commits! just keep your work in a branch
- open a PR and review with someone synchronously before merging to main
## getting started real fast
Here's what to do if you haven't already set up your `GOPATH`:
1. Run `go env` and figure out how your Go installation is configured
with respect to your `GOPATH`:
```shell
$ go env | grep GOPATH
GOPATH='/home/you/go'
```
2. Set up this repository within your `GOPATH`:
```shell
# If GOPATH is /home/you/go, then...
$ cd ~/go && mkdir -p pkg/git.bunk.computer/bunk/smsbb && cd pkg/git.bunk.computer/bunk
$ git clone https://git.bunk.computer/bunk/smsbb
$ cd smsbb
```
3. Get your Twilio Account SID and Auth Token and store them in the following environment
variables:
```shell
$ export SMSBB_ACCT_SID="..." # Your Twilio Account SID goes here
$ export SMSBB_AUTH_TOKEN="..." # Your Twilio Auth Token goes here
```
4. Run `smsbb`:
```shell
# In this example, +1 (828) 555-0123 is the "from" address, and
# +1 (828) 555-4567 is the "to" address
$ go run smsbb.go "+18285550123" "+18285554567" "This is your first message\!"
```

49
smsbb.go Normal file
View file

@ -0,0 +1,49 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
)
func main () {
accountSid := os.Getenv("SMSBB_ACCT_SID")
authToken := os.Getenv("SMSBB_AUTH_TOKEN")
if (len(accountSid) == 0) {
fmt.Fprintf(os.Stderr, "Please set the SMSBB_ACCT_SID environment variable to your Twilio account SID\n")
os.Exit(1)
}
if (len(authToken) == 0) {
fmt.Fprintf(os.Stderr, "Please set the SMSBB_AUTH_TOKEN environment variable to your Twilio authentication token\n")
os.Exit(1)
}
if (len(os.Args) < 4) {
fmt.Fprintf(os.Stderr, "%s FROM-NUMBER TO-NUMBER MESSAGE-BODY\n", os.Args[0])
os.Exit(1)
}
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})
params := &twilioApi.CreateMessageParams{}
params.SetFrom(os.Args[1])
params.SetTo(os.Args[2])
params.SetBody(os.Args[3])
resp, err := client.Api.CreateMessage(params)
if err != nil {
fmt.Fprintf(os.Stderr, "Error sending SMS message: %s", err.Error())
} else {
response, _ := json.Marshal(*resp)
fmt.Printf("Response from Twilio: %s", string(response))
}
}

View file

@ -1,166 +0,0 @@
use aws_config::meta::region::RegionProviderChain;
use aws_config::BehaviorVersion;
use aws_sdk_dynamodb::operation::create_table::CreateTableError;
// use aws_sdk_sns::{Client as SnsClient, Error as SnsError};
use aws_sdk_dynamodb::Client as DynamoClient;
use aws_sdk_dynamodb::types::{AttributeDefinition, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType};
// const TOPIC_PREFIX: &'static str = "smsbb_";
const TABLE_PREFIX: &'static str = "smsbb_";
/*
fn full_topic_name(s: &str) -> String {
format!("{}{}", TOPIC_PREFIX, s)
}
*/
fn table_name_with_prefix(s: &str) -> String {
format!("{}{}", TABLE_PREFIX, s)
}
fn users_table_name() -> String {
table_name_with_prefix("users")
}
fn campaigns_table_name() -> String {
table_name_with_prefix("campaigns")
}
fn messages_table_name() -> String {
table_name_with_prefix("messages")
}
fn unsubscribes_table_name() -> String {
table_name_with_prefix("unsubscribes")
}
#[derive(Debug)]
enum SmsbbInternalError {
CreateTableFailed(String)
}
fn create_table_failed(table_name: &str) -> SmsbbInternalError {
SmsbbInternalError::CreateTableFailed(table_name.to_string())
}
struct DynamoKeySpecification<'a> {
hash_key: &'a str,
sort_key: Option<&'a str>
}
fn build_key_schema_element(attr_name: &str, key_type: KeyType) -> KeySchemaElement {
KeySchemaElement::builder()
.attribute_name(attr_name)
.key_type(key_type)
.build()
.unwrap()
}
fn string_attribute_definition(name: &str) -> AttributeDefinition {
AttributeDefinition::builder().attribute_name(name).attribute_type(ScalarAttributeType::S).build().unwrap()
}
fn atribute_definitions_for_key<'a>(primary_key: &DynamoKeySpecification) -> Option<Vec<AttributeDefinition>> {
let mut results = vec!(string_attribute_definition(primary_key.hash_key));
match primary_key.sort_key {
Some(sort_key) => {
results.push(string_attribute_definition(sort_key))
}
None => {}
}
Some(results)
}
async fn create_table_if_not_exists<'a>(
client: &DynamoClient,
table_name: &str,
primary_key: &DynamoKeySpecification<'a>
) -> Result<(), SmsbbInternalError> {
let primary_key_schema = build_key_schema_element(primary_key.hash_key, KeyType::Hash);
let mut table_builder = client.create_table()
.set_attribute_definitions(atribute_definitions_for_key(primary_key))
.table_name(table_name)
.key_schema(primary_key_schema)
.billing_mode(BillingMode::PayPerRequest);
match primary_key.sort_key {
Some(sort_key) => {
table_builder = table_builder.key_schema(build_key_schema_element(sort_key, KeyType::Range));
}
None => {}
}
let build_result = table_builder.send().await;
match build_result {
Ok(_) => {},
Err(e) => {
match e.as_service_error() {
None => {
dbg!("{}", e);
return Err(create_table_failed(table_name));
},
Some(cte) => {
match cte {
CreateTableError::ResourceInUseException(_) => {
}
problem => {
dbg!("{}", problem);
return Err(create_table_failed(table_name));
}
}
}
}
}
}
// We're not handling secondary indexes yet.
Ok(())
}
async fn ensure_tables(client: &DynamoClient) -> Result<(), SmsbbInternalError> {
let users_table = users_table_name();
let users_key = DynamoKeySpecification { hash_key: "email", sort_key: None };
create_table_if_not_exists(client, &users_table, &users_key).await?;
let campaign_table = campaigns_table_name();
let campaign_key = DynamoKeySpecification { hash_key: "campaign_name", sort_key: Some("created_at") };
create_table_if_not_exists(client, &campaign_table, &campaign_key).await?;
let messages_table = messages_table_name();
let messages_key = DynamoKeySpecification { hash_key: "campaign_name", sort_key: Some("message_id") };
create_table_if_not_exists(client, &messages_table, &messages_key).await?;
let unsubscribes_table = unsubscribes_table_name();
let unsubscribes_key = DynamoKeySpecification { hash_key: "campaign_name", sort_key: Some("phone_number") };
create_table_if_not_exists(client, &unsubscribes_table, &unsubscribes_key).await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), SmsbbInternalError> {
let region_provider = RegionProviderChain::default_provider()
.or_else("us-east-1");
let config = aws_config::defaults(BehaviorVersion::latest())
.region(region_provider)
.load()
.await;
let dynamo_client = DynamoClient::new(&config);
let ensure_resp = ensure_tables(&dynamo_client).await;
match ensure_resp {
Err(sdk_error) => {
dbg!("{}", sdk_error);
return Err(SmsbbInternalError::CreateTableFailed("woof".to_string()));
},
_ => {}
}
Ok(())
}