├── .editorconfig ├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── challenges ├── 1a-hello │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ │ ├── index.js │ │ └── template.js │ ├── tsconfig.json │ └── wrangler.toml ├── 3-discord │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ │ ├── commands │ │ │ └── blep.ts │ │ ├── index.ts │ │ ├── receive.ts │ │ ├── setup.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── wrangler.toml ├── 4-unsplash │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── 6-daynight │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml └── bonus-reminders │ ├── media │ ├── demo.gif │ └── webhook.png │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ ├── database.ts │ ├── index.ts │ ├── twilio.ts │ ├── utils.ts │ └── views.ts │ ├── tsconfig.json │ └── wrangler.toml ├── license ├── package.json ├── pnpm-workspace.yaml ├── readme.md └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,yaml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | - master 9 | schedule: 10 | - cron: '0 0 * * *' 11 | name: Semgrep config 12 | jobs: 13 | semgrep: 14 | name: semgrep/ci 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | SEMGREP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 20 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 21 | container: 22 | image: returntocorp/semgrep 23 | steps: 24 | - uses: actions/checkout@v3 25 | - run: semgrep ci 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *-lock.* 4 | *.lock 5 | 6 | **/build/** 7 | -------------------------------------------------------------------------------- /challenges/1a-hello/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "build/index.js", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "predeploy": "npm run build", 7 | "deploy": "wrangler publish" 8 | }, 9 | "dependencies": { 10 | "country-code-emoji": "2.2.0" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-node-resolve": "11.2.1", 14 | "rollup": "2.45.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /challenges/1a-hello/readme.md: -------------------------------------------------------------------------------- 1 | # Challenge #1A: Hello Worker 2 | 3 | ## Setup 4 | 5 | 1. Install `npm` dependencies 6 | 2. Insert values for the following `wranger.toml` configuration keys: 7 | * `account_id` 8 | * `zone_id` 9 | 10 | ## Deploy 11 | 12 | A local `"deploy"` script is included, found within the `package.json` file. 13 | 14 | This is an alias for `wrangler publish`, but it will also run the `"build"` command before publishing. 15 | 16 | ```sh 17 | $ npm run deploy 18 | ``` 19 | 20 | ## Further Learning 21 | 22 | Check out the [Introduction to Cloudflare Workers](https://egghead.io/courses/introduction-to-cloudflare-workers-5aa3) free video series! 23 | -------------------------------------------------------------------------------- /challenges/1a-hello/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: 'src/index.js', 5 | output: { 6 | format: 'esm', 7 | file: 'build/index.js', 8 | sourcemap: false, 9 | }, 10 | plugins: [ 11 | resolve({ browser: true }) 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /challenges/1a-hello/src/index.js: -------------------------------------------------------------------------------- 1 | import { template } from './template'; 2 | 3 | addEventListener('fetch', event => { 4 | event.respondWith( 5 | handleRequest(event.request).catch(err => { 6 | return new Response(err.stack || 'Unknown Error', { 7 | status: 500 8 | }); 9 | }) 10 | ); 11 | }); 12 | 13 | /** 14 | * @param {Request} request 15 | * @returns {Promise} 16 | */ 17 | async function handleRequest(request) { 18 | const html = template(request.cf); 19 | return new Response(html, { 20 | status: 200, 21 | headers: { 22 | 'content-type': 'text/html;charset=utf8' 23 | }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /challenges/1a-hello/src/template.js: -------------------------------------------------------------------------------- 1 | import flag from 'country-code-emoji'; 2 | 3 | /** 4 | * @param {IncomingRequestCfProperties} cf 5 | * @returns {string} 6 | */ 7 | export function template(cf) { 8 | const { country } = cf; 9 | const emoji = flag(country) || '👋'; 10 | 11 | return ` 12 | 13 | 14 | 15 | 16 | WWCode Worker 17 | 18 | 19 | 36 | 37 | 38 |
39 |

Hello Worker!

40 |

You're connecting from ${country} ${emoji}

41 |
42 | 43 | 44 | `; 45 | } 46 | -------------------------------------------------------------------------------- /challenges/1a-hello/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /challenges/1a-hello/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "hello" 2 | type = "javascript" 3 | workers_dev = true 4 | 5 | route = "" 6 | account_id = "" 7 | zone_id = "" 8 | -------------------------------------------------------------------------------- /challenges/3-discord/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "build/index.js", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "predeploy": "npm run build", 7 | "deploy": "wrangler publish" 8 | }, 9 | "dependencies": { 10 | "tweetnacl": "1.0.3" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-commonjs": "18.0.0", 14 | "@rollup/plugin-node-resolve": "11.2.1", 15 | "@rollup/plugin-typescript": "8.2.1", 16 | "rollup": "2.45.2", 17 | "tslib": "2.2.0", 18 | "typescript": "4.2.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /challenges/3-discord/readme.md: -------------------------------------------------------------------------------- 1 | # Challenge #3: Discord Bots 2 | 3 | ## Setup 4 | 5 | 1. Install `npm` dependencies 6 | 2. Create a [Discord Application](https://discord.com/developers/applications) 7 | 3. Store the Application's Client ID and Public Key values inside the `wrangler.toml` file's `[vars]` config: 8 | * `CLIENT_ID` 9 | * `PUBLICKEY` 10 | 4. Copy the Client Secret value, and then store it as Workers Secret using wrangler: 11 | ```sh 12 | $ wrangler secret put CLIENT_SECRET 13 | ``` 14 | 5. Create a new KV Namespace, saving its ID value inside your `wrangler.toml` file 15 | 16 | 17 | ## Deploy 18 | 19 | A local `"deploy"` script is included, found within the `package.json` file. 20 | 21 | This is an alias for `wrangler publish`, but it will also run the `"build"` command before publishing. 22 | 23 | ```sh 24 | $ npm run deploy 25 | ``` 26 | -------------------------------------------------------------------------------- /challenges/3-discord/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | export default { 6 | input: 'src/index.ts', 7 | output: { 8 | format: 'esm', 9 | file: 'build/index.js', 10 | sourcemap: false, 11 | }, 12 | plugins: [ 13 | resolve({ browser: true }), 14 | commonjs(), 15 | typescript() 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /challenges/3-discord/src/commands/blep.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils'; 2 | 3 | interface Animal { 4 | image: string; 5 | source: string; 6 | } 7 | 8 | function toKeyname(name: string, smol: boolean): string { 9 | let key = `animal::${name.toLowerCase()}`; 10 | if (smol) key += '::smol'; 11 | return key; 12 | } 13 | 14 | // Load KV Namespace w/ values 15 | // @NOTE 16 | // This is called manually during setup and 17 | // is not part of the Discord API or application. 18 | export async function load(): Promise { 19 | const Dataset: { 20 | [name: string]: { 21 | baby: Animal; 22 | adult: Animal; 23 | } 24 | } = { 25 | Puffin: { 26 | adult: { 27 | image: 'https://cdn.pixabay.com/photo/2019/04/22/08/55/puffin-4146015_1280.jpg', 28 | source: 'https://pixabay.com/photos/puffin-bird-nature-penguin-birds-4146015/' 29 | }, 30 | baby: { 31 | image: 'https://i.pinimg.com/564x/fd/e4/95/fde495fccca8ec722be74cd4079f1842.jpg', 32 | source: 'https://www.pinterest.com/pin/123004633544859667/' 33 | }, 34 | }, 35 | Fox: { 36 | adult: { 37 | image: 'https://cdn.pixabay.com/photo/2015/04/10/01/41/fox-715588_1280.jpg', 38 | source: 'https://pixabay.com/photos/fox-nature-animals-roux-fauna-715588/' 39 | }, 40 | baby: { 41 | image: 'https://cdn.pixabay.com/photo/2018/03/11/20/42/mammals-3218028_1280.jpg', 42 | source: 'https://pixabay.com/photos/mammals-fox-wildlife-natural-wild-3218028/' 43 | } 44 | }, 45 | Dog: { 46 | adult: { 47 | image: 'https://cdn.pixabay.com/photo/2015/03/26/09/47/dog-690318_1280.jpg', 48 | source: 'https://pixabay.com/photos/dog-golden-retriever-canine-pet-690318/' 49 | }, 50 | baby: { 51 | image: 'https://cdn.pixabay.com/photo/2016/02/09/12/25/puppy-1189067_1280.jpg', 52 | source: 'https://pixabay.com/photos/puppy-golden-retriever-dog-pet-1189067/' 53 | } 54 | }, 55 | Cat: { 56 | adult: { 57 | image: 'https://cdn.pixabay.com/photo/2017/11/09/21/41/cat-2934720_1280.jpg', 58 | source: 'https://pixabay.com/photos/cat-kitten-pets-animals-housecat-2934720/' 59 | }, 60 | baby: { 61 | image: 'https://cdn.pixabay.com/photo/2017/07/25/01/22/cat-2536662_1280.jpg', 62 | source: 'https://pixabay.com/photos/cat-flower-kitten-stone-pet-2536662/' 63 | } 64 | }, 65 | Chimpanzee: { 66 | adult: { 67 | image: 'https://cdn.pixabay.com/photo/2018/09/25/21/32/chimpanzee-3703230_1280.jpg', 68 | source: 'https://pixabay.com/photos/chimpanzee-monkey-ape-mammal-zoo-3703230/' 69 | }, 70 | baby: { 71 | image: 'https://cdn.pixabay.com/photo/2018/02/05/18/22/chimp-3132852_1280.jpg', 72 | source: 'https://pixabay.com/photos/chimp-baby-monkey-feelings-look-3132852/' 73 | } 74 | }, 75 | Hedgehog: { 76 | adult: { 77 | image: 'https://cdn.pixabay.com/photo/2016/02/22/10/06/hedgehog-1215140_1280.jpg', 78 | source: 'https://pixabay.com/photos/hedgehog-cute-animal-little-nature-1215140/' 79 | }, 80 | baby: { 81 | image: 'https://cdn.pixabay.com/photo/2020/05/09/08/27/hedgehog-5148711_1280.jpg', 82 | source: 'https://pixabay.com/photos/hedgehog-young-animal-animal-world-5148711/' 83 | } 84 | }, 85 | Chicken: { 86 | adult: { 87 | image: 'https://cdn.pixabay.com/photo/2019/04/24/08/48/chicken-4151637_1280.jpg', 88 | source: 'https://pixabay.com/photos/chicken-freiland-chicken-animal-4151637/' 89 | }, 90 | baby: { 91 | image: 'https://cdn.pixabay.com/photo/2014/05/20/21/20/bird-349026_1280.jpg', 92 | source: 'https://pixabay.com/photos/bird-chicks-baby-chicken-young-bird-349026/' 93 | } 94 | } 95 | }; 96 | 97 | const names = Object.keys(Dataset).sort(); 98 | 99 | await ANIMALS.put( 100 | 'animal::choices', 101 | JSON.stringify(names) 102 | ); 103 | 104 | for (const name of names) { 105 | let images = Dataset[name]; 106 | 107 | await Promise.all([ 108 | ANIMALS.put( 109 | toKeyname(name, true), 110 | JSON.stringify(images.baby) 111 | ), 112 | ANIMALS.put( 113 | toKeyname(name, false), 114 | JSON.stringify(images.adult) 115 | ) 116 | ]); 117 | } 118 | } 119 | 120 | // The command definition 121 | // @see https://discord.com/developers/docs/interactions/slash-commands#registering-a-command 122 | export async function command() { 123 | // Retrieve all `Animal` names from KV 124 | const names = await ANIMALS.get('animal::choices', 'json') || []; 125 | 126 | return { 127 | name: 'blep', 128 | description: 'Send a random adorable animal photo', 129 | options: [ 130 | { 131 | name: 'animal', 132 | description: 'The type of animal', 133 | required: true, 134 | type: 3, 135 | choices: names.map(str => { 136 | return { 137 | name: str, 138 | value: str.toLowerCase() 139 | } 140 | }) 141 | }, { 142 | name: 'only_smol', 143 | description: 'Whether to show only baby animals', 144 | required: false, 145 | type: 5, 146 | } 147 | ] 148 | }; 149 | } 150 | 151 | /** 152 | * Handle the slash command's input 153 | * @param {Option[]} options The command's selected options. 154 | */ 155 | export async function handler(options: Option[]): Promise { 156 | // Convert `options` to object 157 | let values: Record = {}; 158 | options.forEach(obj => values[obj.name] = obj.value); 159 | 160 | // Construct key name 161 | let key = toKeyname( 162 | values.animal as string, 163 | values.only_smol as boolean 164 | ); 165 | 166 | const value = await ANIMALS.get(key, 'json'); 167 | if (!value) return utils.respond(404, 'Invalid choice(s)'); 168 | 169 | let alttext = 'A '; 170 | if (values.only_smol) alttext += 'baby '; 171 | alttext += values.animal; 172 | 173 | // @see https://discord.com/developers/docs/interactions/slash-commands#InteractionApplicationCommandCallbackData 174 | return utils.respond(200, { 175 | type: 4, 176 | data: { 177 | embeds: [{ 178 | type: 'image', 179 | description: alttext, 180 | provider: { url: value.source }, 181 | image: { url: value.image }, 182 | }], 183 | }, 184 | }); 185 | } 186 | -------------------------------------------------------------------------------- /challenges/3-discord/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as setup from './setup'; 2 | import { interaction } from './receive'; 3 | import * as utils from './utils'; 4 | 5 | declare global { 6 | // @see https://discord.com/developers/applications 7 | const CLIENT_ID: string; 8 | const CLIENT_SECRET: string; 9 | const PUBLICKEY: string; 10 | 11 | // Our KV Namespace 12 | const ANIMALS: KVNamespace; 13 | 14 | // Discord struct(s) 15 | interface Option { 16 | name: string; 17 | value: unknown; 18 | } 19 | } 20 | 21 | async function handler(req: Request): Promise { 22 | const { pathname } = new URL(req.url); 23 | 24 | // Receiving a Discord user's slash command 25 | if (req.method === 'POST' && pathname === '/interaction') { 26 | return interaction(req); 27 | } 28 | 29 | // Redirect for App setup/authorization 30 | // IMPORTANT: You must call this manually, including after App updates! 31 | // NOTE: You can remove this after installation 32 | if (req.method === 'GET' && pathname === '/') { 33 | return setup.authorize(); 34 | } 35 | 36 | // Add and/or update the App's command definitions 37 | // IMPORTANT: You must call this manually, including after App updates! 38 | // NOTE: You can remove this after installation 39 | if (req.method === 'GET' && pathname === '/setup') { 40 | return setup.commands(); 41 | } 42 | 43 | return utils.respond(400, 'Unknown Request'); 44 | } 45 | 46 | addEventListener('fetch', event => { 47 | event.respondWith( 48 | handler(event.request) 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /challenges/3-discord/src/receive.ts: -------------------------------------------------------------------------------- 1 | import { sign } from 'tweetnacl'; 2 | import * as blep from './commands/blep'; 3 | import * as utils from './utils'; 4 | 5 | export async function interaction(req: Request): Promise { 6 | try { 7 | // 1. Validating incoming request signature 8 | // @see https://discord.com/developers/docs/interactions/slash-commands#security-and-authorization 9 | const timestamp = req.headers.get('X-Signature-Timestamp') || ''; 10 | const signature = req.headers.get('X-Signature-Ed25519') || ''; 11 | const rawBody = await req.clone().text(); 12 | 13 | const isVerified = sign.detached.verify( 14 | utils.encode(timestamp + rawBody), 15 | utils.viaHEX(signature), 16 | utils.viaHEX(PUBLICKEY), 17 | ); 18 | 19 | if (!isVerified) throw 1; 20 | } catch (err) { 21 | return utils.respond(401, 'Invalid request signature'); 22 | } 23 | 24 | try { 25 | // 2. Determine the interaction type 26 | // @see https://discord.com/developers/docs/interactions/slash-commands#receiving-an-interaction 27 | const action = await req.json(); 28 | 29 | // PING (required) 30 | if (action.type == 1) { 31 | return utils.respond(200, { type: 1 }); 32 | } 33 | 34 | // APP COMMAND(S) 35 | // TODO: Add your own handler(s) here! 36 | // TODO: Determine `handler` by matching on command name (aka `action.data.name`) 37 | // const { user } = action.member; 38 | // const { name, options=[] } = action.data; 39 | return await blep.handler(action.data.options); 40 | } catch (err) { 41 | return utils.respond(400, 'Error handling interaction'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /challenges/3-discord/src/setup.ts: -------------------------------------------------------------------------------- 1 | import * as utils from './utils'; 2 | import * as blep from './commands/blep'; 3 | 4 | /** 5 | * Redirect to an App Authorization screen 6 | * @IMPORTANT One must authorize this App manually! 7 | * @NOTE This route may be removed from the Worker Script once authorized. 8 | * @see https://discord.com/developers/docs/interactions/slash-commands#authorizing-your-application 9 | * @see https://discord.com/developers/docs/topics/oauth2 10 | */ 11 | export function authorize(): Response { 12 | const target = new URL('https://discord.com/api/oauth2/authorize'); 13 | target.searchParams.set('scope', 'applications.commands'); 14 | target.searchParams.set('client_id', CLIENT_ID); 15 | return Response.redirect(target.href, 302); 16 | } 17 | 18 | /** 19 | * Add/Synchronize Slash Commands for the Application. 20 | * @NOTE Discord requires 1 hour to reflect command changes. 21 | * @see https://discord.com/developers/docs/interactions/slash-commands#bulk-overwrite-global-application-commands 22 | */ 23 | export async function commands(): Promise { 24 | try { 25 | // 1. Exchange client credentials for `Bearer` token 26 | // @see https://discord.com/developers/docs/topics/oauth2 27 | const res = await fetch('https://discord.com/api/v6/oauth2/token', { 28 | method: 'POST', 29 | body: new URLSearchParams({ 30 | grant_type: 'client_credentials', 31 | scope: 'applications.commands.update', 32 | }), 33 | headers: { 34 | 'Authorization': `Basic ${btoa(CLIENT_ID + ':' + CLIENT_SECRET)}`, 35 | 'Content-Type': 'application/x-www-form-urlencoded', 36 | }, 37 | }); 38 | 39 | const data = await res.json(); 40 | var token = `Bearer ${data.access_token}`; 41 | } catch (err) { 42 | return utils.respond(400, 'Error fetching Authorization token'); 43 | } 44 | 45 | try { 46 | // 2. Bulk Overwrite Application Commands! 47 | const res = await fetch(`https://discord.com/api/v8/applications/${CLIENT_ID}/commands`, { 48 | method: 'PUT', 49 | headers: { 50 | 'Authorization': token, 51 | 'Content-Type': 'application/json' 52 | }, 53 | body: JSON.stringify([ 54 | await blep.command() 55 | ]) 56 | }); 57 | 58 | if (res.ok) { 59 | return utils.respond(200, 'OK'); 60 | } 61 | 62 | return utils.respond(400, 'Error synchronizing command definition(s)'); 63 | } catch (err) { 64 | return utils.respond(400, 'Error initializing request'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /challenges/3-discord/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @source worktop/utils 3 | * @see https://github.com/lukeed/worktop/blob/master/src/utils.ts 4 | * @license MIT 5 | */ 6 | 7 | export const Encoder = /*#__PURE__*/ new TextEncoder; 8 | export const encode = (input: string) => Encoder.encode(input); 9 | 10 | export function viaHEX(input: string): Uint8Array { 11 | let i=0, len=input.length, out: number[] = []; 12 | 13 | if (len & 1) { 14 | input += '0'; 15 | len++; 16 | } 17 | 18 | for (; i < len; i+=2) { 19 | out.push(parseInt(input[i] + input[i+1], 16)); 20 | } 21 | 22 | return new Uint8Array(out); 23 | } 24 | 25 | /** 26 | * Tiny `Response` formatter/helper. 27 | * @param status The response status code 28 | * @param data The response body 29 | */ 30 | export function respond(status: number, data: any): Response { 31 | let headers = new Headers; 32 | if (data && typeof data === 'object') { 33 | headers.set('Content-Type', 'application/json'); 34 | data = JSON.stringify(data); 35 | } 36 | return new Response(data, { status, headers }); 37 | } 38 | -------------------------------------------------------------------------------- /challenges/3-discord/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /challenges/3-discord/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "discord" 2 | type = "javascript" 3 | workers_dev = true 4 | 5 | route = "" 6 | account_id = "" 7 | zone_id = "" 8 | 9 | [vars] 10 | PUBLICKEY = "" 11 | CLIENT_ID = "" 12 | # CLIENT_SECRET ~> wrangler secret put CLIENT_SECRET 13 | 14 | [[kv_namespaces]] 15 | binding = "ANIMALS" 16 | preview_id = "" 17 | id = "" 18 | -------------------------------------------------------------------------------- /challenges/4-unsplash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "build/index.js", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "predeploy": "npm run build", 7 | "deploy": "wrangler publish" 8 | }, 9 | "devDependencies": { 10 | "@rollup/plugin-typescript": "8.2.1", 11 | "rollup": "2.45.2", 12 | "tslib": "2.2.0", 13 | "typescript": "4.2.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /challenges/4-unsplash/readme.md: -------------------------------------------------------------------------------- 1 | # Challenge #4: Random Rendering 2 | 3 | ## Setup 4 | 5 | 1. Install `npm` dependencies 6 | 2. Create an [Unsplash Application](https://unsplash.com/oauth/applications/new) 7 | 3. Store the Application's Access Key as the `ACCESSKEY` binding within your `wrangler.toml` file 8 | 9 | 10 | ## Deploy 11 | 12 | A local `"deploy"` script is included, found within the `package.json` file. 13 | 14 | This is an alias for `wrangler publish`, but it will also run the `"build"` command before publishing. 15 | 16 | ```sh 17 | $ npm run deploy 18 | ``` 19 | -------------------------------------------------------------------------------- /challenges/4-unsplash/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default { 4 | input: 'src/index.ts', 5 | output: { 6 | format: 'esm', 7 | file: 'build/index.js', 8 | sourcemap: false, 9 | }, 10 | plugins: [ 11 | typescript() 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /challenges/4-unsplash/src/index.ts: -------------------------------------------------------------------------------- 1 | // @see https://unsplash.com/oauth/applications/new 2 | declare const ACCESSKEY: string; 3 | 4 | // Apply incoming query parameter(s) onto a new URL instance, if present in source 5 | function forward(source: URLSearchParams, target: URLSearchParams, ...names: string[]) { 6 | names.forEach(name => { 7 | let value = source.get(name); 8 | if (value != null) target.set(name, value); 9 | }); 10 | } 11 | 12 | async function handle(req: Request): Promise { 13 | const INPUT = new URL(req.url).searchParams; 14 | 15 | // Request a random image from the Unsplash API 16 | // @see https://unsplash.com/documentation#get-a-random-photo 17 | const UNSPLASH = new URL('https://api.unsplash.com/photos/random'); 18 | forward(INPUT, UNSPLASH.searchParams, 'orientation', 'content_filter', 'collections', 'username', 'query'); 19 | 20 | // TODO: Handle Rate Limiting Error(s) 21 | // @see https://unsplash.com/documentation#rate-limiting 22 | const res = await fetch(UNSPLASH.href, { 23 | method: 'GET', 24 | headers: { 25 | 'Accept-Version': 'v1', 26 | 'Authorization': `Client-ID ${ACCESSKEY}`, 27 | } 28 | }); 29 | 30 | const { urls } = await res.json(); 31 | 32 | // Pick a size to use via `?size=value` 33 | // Options: "regular" | "small" | "thumb" 34 | // Default: "regular" 35 | let size = INPUT.get('size'); 36 | 37 | if (size && /^(regular|small|thumb)$/i.test(size)) { 38 | size = size.toLowerCase(); 39 | } else { 40 | size = 'regular'; 41 | } 42 | 43 | // Select the root image address 44 | const IMAGE = new URL(urls[size]); 45 | 46 | // Apply any imgix global parameters 47 | // @see https://unsplash.com/documentation#supported-parameters 48 | // @example https://?orientation=landscape&size=small&fm=webp 49 | forward(INPUT, IMAGE.searchParams, 'w', 'h', 'crop', 'fm', 'auto', 'q', 'fit', 'dpr'); 50 | 51 | return Response.redirect(IMAGE.href, 302); 52 | 53 | // For CORS programmatic requests: 54 | // return new Response(null, { 55 | // status: 302, 56 | // headers: { 57 | // 'Location': IMAGE.href, 58 | // 'Access-Control-Max-Age': '86400', 59 | // 'Access-Control-Allow-Origin': '*', 60 | // 'Access-Control-Allow-Methods': 'GET', 61 | // } 62 | // }); 63 | } 64 | 65 | addEventListener('fetch', event => { 66 | event.respondWith( 67 | handle(event.request) 68 | ); 69 | }); 70 | -------------------------------------------------------------------------------- /challenges/4-unsplash/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /challenges/4-unsplash/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "unsplash" 2 | type = "javascript" 3 | workers_dev = true 4 | 5 | route = "" 6 | account_id = "" 7 | zone_id = "" 8 | 9 | [vars] 10 | ACCESSKEY = "" 11 | -------------------------------------------------------------------------------- /challenges/6-daynight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "build/index.js", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "predeploy": "npm run build", 7 | "deploy": "wrangler publish" 8 | }, 9 | "devDependencies": { 10 | "@rollup/plugin-typescript": "8.2.1", 11 | "rollup": "2.45.2", 12 | "tslib": "2.2.0", 13 | "typescript": "4.2.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /challenges/6-daynight/readme.md: -------------------------------------------------------------------------------- 1 | # Challenge #6: Day and Night 2 | 3 | ## Setup 4 | 5 | 1. Install `npm` dependencies 6 | 2. Optionally update the `LOCALE_DEFAULT` value within the `wrangler.toml` file.
_**Note:** This acts as a fallback value for when the incoming request is missing the `Accept-Language` header._ 7 | 8 | 9 | ## Deploy 10 | 11 | A local `"deploy"` script is included, found within the `package.json` file. 12 | 13 | This is an alias for `wrangler publish`, but it will also run the `"build"` command before publishing. 14 | 15 | ```sh 16 | $ npm run deploy 17 | ``` 18 | -------------------------------------------------------------------------------- /challenges/6-daynight/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default { 4 | input: 'src/index.ts', 5 | output: { 6 | format: 'esm', 7 | file: 'build/index.js', 8 | sourcemap: false, 9 | }, 10 | plugins: [ 11 | typescript() 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /challenges/6-daynight/src/index.ts: -------------------------------------------------------------------------------- 1 | // @see wranger.toml 2 | declare const LOCALE_DEFAULT: string; 3 | 4 | async function handle(req: Request): Promise { 5 | const lang = req.headers.get('Accept-Language'); 6 | const locale = lang && lang.split(',')[0] || LOCALE_DEFAULT; 7 | 8 | const timeZone = req.cf.timezone; // eg "America/New_York" 9 | 10 | const localtime = new Date( 11 | new Date().toLocaleString(undefined, { timeZone }) 12 | ); 13 | 14 | const hour = localtime.getHours(); 15 | 16 | // Morning vs Afternoon vs Night 17 | let image='', label='', source=''; 18 | 19 | if (hour >= 5 && hour < 12) { 20 | label = 'Morning'; 21 | source = 'https://unsplash.com/photos/-G3rw6Y02D0'; 22 | image = 'https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=2250&q=80'; 23 | } else if (hour >= 12 && hour < 18) { 24 | label = 'Afternoon'; 25 | source = 'https://unsplash.com/photos/PXQkzW7HbM4'; 26 | image = 'https://images.unsplash.com/photo-1581205445756-15c1d2e9a8df?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=3272&q=80'; 27 | } else { 28 | label = 'Night'; 29 | source = 'https://unsplash.com/photos/Z5ARQ6WNEqA'; 30 | image = 'https://images.unsplash.com/photo-1591554338378-6dcc422b8249?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2250&q=80'; 31 | } 32 | 33 | const res = await fetch('https://example.com'); 34 | const rewriter = new HTMLRewriter; 35 | 36 | rewriter.on('head', { 37 | element(elem) { 38 | elem.append(` 39 | 66 | `, { 67 | html: true 68 | }); 69 | } 70 | }) 71 | 72 | rewriter.on('body', { 73 | element(elem) { 74 | elem.setInnerContent(` 75 |
76 |

Good ${label}!

77 |

78 | It is currently 79 | 84 |

85 | Image via Unsplash 86 |
87 | `, { 88 | html: true 89 | }); 90 | } 91 | }); 92 | 93 | return rewriter.transform(res); 94 | } 95 | 96 | addEventListener('fetch', event => { 97 | event.respondWith( 98 | handle(event.request) 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /challenges/6-daynight/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /challenges/6-daynight/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "daynight" 2 | type = "javascript" 3 | workers_dev = true 4 | 5 | route = "" 6 | account_id = "" 7 | zone_id = "" 8 | 9 | [build] 10 | command = "npm run build" 11 | upload.format = "service-worker" 12 | 13 | [vars] 14 | LOCALE_DEFAULT = "en-US" 15 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/devweek/e56debe29e7f5e9a63a7eba0fec5f87e13c9d3e8/challenges/bonus-reminders/media/demo.gif -------------------------------------------------------------------------------- /challenges/bonus-reminders/media/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/devweek/e56debe29e7f5e9a63a7eba0fec5f87e13c9d3e8/challenges/bonus-reminders/media/webhook.png -------------------------------------------------------------------------------- /challenges/bonus-reminders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "build/index.js", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "predeploy": "npm run build", 7 | "deploy": "wrangler publish" 8 | }, 9 | "devDependencies": { 10 | "@rollup/plugin-typescript": "8.2.1", 11 | "rollup": "2.45.2", 12 | "tslib": "2.2.0", 13 | "typescript": "4.2.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/readme.md: -------------------------------------------------------------------------------- 1 | # Bonus Challenge: Twilio Reminders 2 | 3 | ## Setup 4 | 5 | 1. Install `npm` dependencies 6 | 2. Create a [Twilio Account](https://www.twilio.com/console), including phone number verification 7 | 3. Update the following `[vars]` values within your `wrangler.toml` file: 8 | * `TEST_RECIPIENT` – this is *your* personal phone number that Twilio asked you to verify 9 | * `TWILIO_PHONENUMBER` – this is the phone number that Twilio provided you 10 | * `TWILIO_ACCOUNTSID` – your Account SID, found in the [Twilio Console](https://www.twilio.com/console) 11 | 4. Save your Twilio Auth Token, found in the [Twilio Console](https://www.twilio.com/console), as a Worker Secret: 12 | ```sh 13 | $ wrangler secret put TWILIO_AUTHTOKEN 14 | ``` 15 | 5. Attach your endpoint as an Incoming Message webhook in your [Twilio number's settings](https://www.twilio.com/console/phone-numbers/incoming): 16 | * Click on your Twilio phone number 17 | * Scroll down to the **Messaging** section 18 | * Enter `POST https://DOMAIN/webhook` as your webhook target, where `DOMAIN` is your domain.
_**Note:** This Worker script provisions the `POST /webhook` route for you._
19 | 20 | 21 | ## Deploy 22 | 23 | A local `"deploy"` script is included, found within the `package.json` file. 24 | 25 | This is an alias for `wrangler publish`, but it will also run the `"build"` command before publishing. 26 | 27 | ```sh 28 | $ npm run deploy 29 | ``` 30 | 31 | ## Usage 32 | 33 | Once deployed, visit your domain root and enter your _personal_ phone number, which you verified with Twilio. A SMS message will be sent to you with further steps. 34 | 35 | > **Note:** The Twilio application will only work with this number for the duration of your Twilio trial. 36 | 37 | ***Example Demonstration*** 38 | 39 | 40 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default { 4 | input: 'src/index.ts', 5 | output: { 6 | format: 'esm', 7 | file: 'build/index.js', 8 | sourcemap: false, 9 | }, 10 | plugins: [ 11 | typescript() 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/src/database.ts: -------------------------------------------------------------------------------- 1 | declare const REMINDERS: KVNamespace; 2 | 3 | type KEY = string; 4 | 5 | interface Reminder { 6 | id: number; 7 | text: string; 8 | } 9 | 10 | export function toKey(phone: string, country: string): KEY { 11 | return `${country}::${phone.substring(1)}` as KEY; 12 | } 13 | 14 | export function display(items: Reminder[]): string { 15 | let len = items.length, output=''; 16 | 17 | if (len > 0) { 18 | output += `You have ${len} reminder`; 19 | if (len !== 1) output += 's'; 20 | output += ':\n'; 21 | 22 | items.forEach(tmp => { 23 | output += `· (${tmp.id}) ${tmp.text}\n`; 24 | }); 25 | } else { 26 | output += 'You have no reminders.\n'; 27 | } 28 | 29 | output += '\nReply with "NEW " to add a reminder.'; 30 | if (len > 0) output += '\nReply with "DONE " to remove a reminder.'; 31 | 32 | return output; 33 | } 34 | 35 | export function load(key: KEY): Promise { 36 | return REMINDERS.get(key, 'json').then(x => x || []); 37 | } 38 | 39 | // List expires after 7 days (seconds) 40 | const expirationTtl = 60 * 60 * 24 * 7; 41 | export function save(key: KEY, arr: Reminder[]): Promise { 42 | return REMINDERS.put(key, JSON.stringify(arr), { expirationTtl }); 43 | } 44 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as views from './views'; 2 | import * as twilio from './twilio'; 3 | import * as database from './database'; 4 | import * as utils from './utils'; 5 | 6 | // The account's verified phone number 7 | // NOTE: 8 | // Trial accounts can only interact with the 9 | // phone number that you verified for the account. 10 | declare const TEST_RECIPIENT: string; 11 | 12 | async function handle(req: Request): Promise { 13 | const { method, url } = req; 14 | const { pathname } = new URL(url); 15 | 16 | /** 17 | * GET / 18 | * render homepage with form 19 | */ 20 | if (method === 'GET' && pathname === '/') { 21 | return utils.render(views.welcome); 22 | } 23 | 24 | /** 25 | * POST /login 26 | * Begin/Wakeup the SMS workflow 27 | */ 28 | if (method === 'POST' && pathname === '/login') { 29 | const input = await req.text(); 30 | 31 | let phone = new URLSearchParams(input).get('phone'); 32 | if (!phone) return utils.error(400, 'A phone number is required'); 33 | 34 | phone = utils.toE164(phone); 35 | let { country } = req.cf; 36 | 37 | // TODO: Support other countries 38 | // Requires Twilio account configuration 39 | if (country !== 'US') { 40 | return utils.error(403, 'This demo only supports US phone numbers'); 41 | } 42 | 43 | if (phone[1] !== '1' || phone.length < 12) { 44 | phone = '+1' + phone.substring(1); 45 | } 46 | 47 | if (!utils.isE164(phone)) { 48 | return utils.error(422, 'Invalid phone number'); 49 | } 50 | 51 | let info = await twilio.lookup(phone, country); 52 | if (!info.ok) return info; 53 | 54 | let data = await info.json(); 55 | country = data.country_code as string; 56 | phone = data.phone_number as string; 57 | 58 | let key = database.toKey(phone, country); 59 | let text = await database.load(key).then(database.display); 60 | await twilio.sms(phone, text); 61 | 62 | return utils.render(views.sent); 63 | } 64 | 65 | /** 66 | * POST /webhook 67 | * Receive SMS Message events from Twilio 68 | * @NOTE Route is configured via Twilio Console 69 | */ 70 | if (method === 'POST' && pathname === '/webhook') { 71 | const input = await req.text(); 72 | if (!input) return utils.error(400, 'Missing parameters'); 73 | 74 | const signature = req.headers.get('X-Twilio-Signature'); 75 | if (!signature) return utils.error(400, 'Missing Twilio signature'); 76 | 77 | const params = new URLSearchParams(input); 78 | const repro = await twilio.sign(req.url, params); 79 | if (repro !== signature) return utils.error(400, 'Invalid signature'); 80 | 81 | const phone = (params.get('From') || '').trim(); 82 | if (!phone) return utils.error(400, 'Missing "From" parameter'); 83 | if (!utils.isE164(phone)) return utils.error(400, 'Invalid "From" parameter'); 84 | 85 | const country = (params.get('FromCountry') || '').trim(); 86 | if (!country) return utils.error(400, 'Missing "FromCountry" parameter'); 87 | 88 | const Body = (params.get('Body') || '').trim(); 89 | if (!Body) return utils.error(400, 'Missing message text'); 90 | 91 | const [cmd, ...rest] = Body.split(/\s+/); 92 | const KEY = database.toKey(phone, country); 93 | const command = cmd.toUpperCase(); 94 | 95 | if (command === 'LIST') { 96 | let items = await database.load(KEY); 97 | return new Response(database.display(items)); 98 | } 99 | 100 | if (command === 'NEW') { 101 | let list = await database.load(KEY); 102 | 103 | list.push({ 104 | id: list.length + 1, 105 | text: rest.join(' '), 106 | }); 107 | 108 | await database.save(KEY, list); 109 | 110 | let text = list.length + ' reminder' + (list.length === 1 ? '' : 's'); 111 | return new Response(`Added new reminder!\n\nYou now have ${text}.`); 112 | } 113 | 114 | if (command === 'DONE') { 115 | let target = Number(rest.shift()); 116 | let list = await database.load(KEY); 117 | 118 | for (let i=0; i < list.length; i++) { 119 | if (list[i].id === target) { 120 | list.splice(i, 1); 121 | break; 122 | } 123 | } 124 | 125 | await database.save(KEY, list); 126 | 127 | let text = list.length + ' reminder' + (list.length === 1 ? '' : 's'); 128 | return new Response(`Removed reminder!\n\nYou now have ${text} remaining.`); 129 | } 130 | 131 | return utils.error(404, `Unknown "${command}" command`); 132 | } 133 | 134 | return utils.error(404, 'Page Not Found'); 135 | } 136 | 137 | addEventListener('fetch', event => { 138 | event.respondWith( 139 | handle(event.request) 140 | ); 141 | }); 142 | 143 | addEventListener('scheduled', event => { 144 | event.waitUntil( 145 | // NOTE: 146 | // With a trial account, only the 147 | // account's verified phone number can interact 148 | // with Twilio SMS messages. 149 | // TODO: 150 | // Insert your number OR iterate over all KV entries. 151 | database.load( 152 | database.toKey(TEST_RECIPIENT, 'US') 153 | ).then(async arr => { 154 | let text = 'Good morning!\n' + database.display(arr); 155 | if (arr.length) await twilio.sms(TEST_RECIPIENT, text); 156 | return new Response('OK'); 157 | }) 158 | ); 159 | }); 160 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/src/twilio.ts: -------------------------------------------------------------------------------- 1 | import * as utils from './utils'; 2 | 3 | // @see https://www.twilio.com/console 4 | declare const TWILIO_PHONENUMBER: string; 5 | declare const TWILIO_ACCOUNTSID: string; 6 | declare const TWILIO_AUTHTOKEN: string; 7 | 8 | // Prepare HTTP request values for the Twilio API 9 | // @see https://www.twilio.com/docs/usage/api#working-with-twilios-apis 10 | const Authorization = `Basic ${btoa(TWILIO_ACCOUNTSID + ':' + TWILIO_AUTHTOKEN)}`; 11 | 12 | // @see twilio.com/docs/sms/send-messages 13 | export function sms(phone: string, message: string) { 14 | const data = new URLSearchParams; 15 | data.set('From', TWILIO_PHONENUMBER); 16 | data.set('To', utils.toE164(phone)); //=> +1234567890 17 | data.set('Body', message); 18 | 19 | // @see https://www.twilio.com/docs/sms/api#base-url 20 | return fetch(`https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNTSID}/Messages.json`, { 21 | headers: { Authorization }, 22 | method: 'POST', 23 | body: data 24 | }); 25 | } 26 | 27 | // @see https://www.twilio.com/docs/lookup/api#api-url 28 | export function lookup(phone: string, country?: string) { 29 | let url = `https://lookups.twilio.com/v1/PhoneNumbers/${phone}`; 30 | if (country) url += `?CountryCode=${country}`; 31 | 32 | return fetch(url, { 33 | headers: { Authorization }, 34 | method: 'GET', 35 | }); 36 | } 37 | 38 | // Replicate the signing process 39 | // @see https://www.twilio.com/docs/usage/security#validating-requests 40 | export async function sign(url: string, params: URLSearchParams) { 41 | const Encoder = new TextEncoder; 42 | const keydata = Encoder.encode(TWILIO_AUTHTOKEN); 43 | 44 | const HMAC: HmacKeyGenParams = { name: 'HMAC', hash: 'SHA-1' }; 45 | const key = await crypto.subtle.importKey('raw', keydata, HMAC, false, ['sign']); 46 | 47 | params.sort(); 48 | let value = url; 49 | for (let [k, v] of params) { 50 | value += k + v; 51 | } 52 | 53 | const signature = await crypto.subtle.sign(HMAC, key, Encoder.encode(value)); 54 | 55 | return btoa( 56 | String.fromCharCode(...new Uint8Array(signature)) 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as views from './views'; 2 | 3 | const headers = { 4 | 'content-type': 'text/html;charset=utf8' 5 | }; 6 | 7 | export function render(html: string, status = 200) { 8 | return new Response(html, { status, headers }); 9 | } 10 | 11 | export function error(status: number, reason: string) { 12 | let html = views.error(status, reason); 13 | return new Response(html, { status, headers }); 14 | } 15 | 16 | 17 | // Convert a number into E164 format 18 | // @see https://www.twilio.com/docs/glossary/what-e164 19 | export const isE164 = (phone: string) => /^\+[1-9]\d{1,14}$/.test(phone); 20 | export const toE164 = (phone: string) => '+' + phone.replace(/[^\d]/g, ''); 21 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/src/views.ts: -------------------------------------------------------------------------------- 1 | interface Template { 2 | title: string; 3 | message: string; 4 | styles?: string; 5 | body?: string; 6 | } 7 | 8 | function template(options: Template): string { 9 | return ` 10 | 11 | 12 | 13 | Twilio Reminders 14 | 15 | 16 | 17 | 57 | ${options.styles ? options.styles.trim() : ''} 58 | 59 | 60 |
61 |

${options.title}

62 | ${options.message} 63 | ${options.body ? options.body.trim() : ''} 64 |
65 | 66 | 67 | `.trim(); 68 | } 69 | 70 | /** 71 | * The root/home page 72 | */ 73 | export const welcome = template({ 74 | title: 'Twilio Reminders', 75 | message: 'NOTE: Trial accounts may only send messages to their own verified number.', 76 | styles: ` 77 | 89 | `, 90 | body: ` 91 |
92 | 102 | 103 | 106 |
107 | `, 108 | }); 109 | 110 | /** 111 | * Create an Error page 112 | */ 113 | export function error(status: number, reason: string): string { 114 | return template({ 115 | message: reason, 116 | title: `Error (${status})`, 117 | styles: ``, 118 | }); 119 | } 120 | 121 | /** 122 | * message sent screen 123 | */ 124 | export const sent = template({ 125 | title: 'Success!', 126 | message: 'Please check your phone for a new SMS message.', 127 | styles: ``, 128 | }); 129 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /challenges/bonus-reminders/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "reminders" 2 | type = "javascript" 3 | workers_dev = true 4 | 5 | route = "" 6 | account_id = "" 7 | zone_id = "" 8 | 9 | [build] 10 | command = "npm run build" 11 | upload.format = "service-worker" 12 | 13 | [vars] 14 | TEST_RECIPIENT = "+1" # your verified phone number 15 | TWILIO_PHONENUMBER = "+1" # your Twilio phone number 16 | TWILIO_ACCOUNTSID = "" # your Twilio Account SID 17 | # (SECRET) TWILIO_AUTHTOKEN -> your Twilio Auth Token 18 | # $ wrangler secret put TWILIO_AUTHTOKEN 19 | 20 | [triggers] 21 | crons = ["0 16 * * *"] # 4PM (UTC) ~> 9AM (PST) 22 | 23 | [[kv_namespaces]] 24 | binding = "REMINDERS" 25 | preview_id = "" 26 | id = "" 27 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cloudflare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "tsc" 5 | }, 6 | "devDependencies": { 7 | "@cloudflare/workers-types": "2.2.1", 8 | "typescript": "4.2.4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'challenges/**' 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Developer Challenges 2 | 3 | > A collection of _example_ solutions for the [Developer Challenges](https://blog.cloudflare.com/developer-week-challenges/). 4 | 5 | Throughout Developer Week, we have offered a series of challenges to give you the excuse – or nudge – you may have needed to play with Cloudflare's products. 6 | 7 | Within this repository, you'll find _example_ solutions for the challenges. Please note, these contents are meant to serve as reference material only. There are no right or wrong answers! The Developer Challenges were designed to be open-ended, so if your approach looks different – that's great! 8 | 9 | ## Challenges 10 | 11 | ### 1A – Hello Worker 12 | 13 | > **Example Solution:** [`/challenges/1a-hello`](/challenges/1a-hello) 14 | 15 | Deploy your first Cloudflare Worker with Wrangler. 16 | 17 | 18 | ### 1B – Concert Promoter 19 | 20 | > **Example Solution:** TBA – Challenge lasts all week! 21 | 22 | Music fans from all over the world visit your site to find the soonest concert nearest to their location. When your page is requested, render a list of upcoming events, reacting to the client's current location. 23 | 24 | 25 | ### 2 – Pick a Framework 26 | 27 | > **Example Solution:** [`signalnerve/my-nextjs-blog`](https://github.com/signalnerve/my-nextjs-blog)
28 | > **Walkthrough:** [Deploy a Next.js Website with Pages](https://developers.cloudflare.com/pages/how-to/deploy-a-nextjs-site) 29 | 30 | Choose your own framework and build a site for your blogs. 31 | 32 | 33 | ### 3 – Discord Bot 34 | 35 | > **Example Solution:** [`/challenges/3-discord`](/challenges/3-discord) 36 | 37 | Build a Discord Bot with Cloudflare Workers and Workers KV. 38 | 39 | 40 | ### 4 – Random Rendering 41 | 42 | > **Example Solution:** [`/challenges/4-unsplash`](/challenges/4-unsplash) 43 | 44 | Using the UnSplash API, render random images. 45 | 46 | 47 | ### 6 – Day and Night 48 | 49 | > **Example Solution:** [`/challenges/6-daynight`](/challenges/6-daynight) 50 | 51 | Display a different background photo or colour depending on the incoming request's location. 52 | 53 | 54 | ### BONUS – Twilio Reminders 55 | 56 | > **Example Solution:** [`/challenges/bonus-reminders`](/challenges/bonus-reminders) 57 | 58 | Build a Reminders App using Cron triggers and the Twilio API. 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": ["esnext", "dom.iterable", "webworker"], 8 | "types": ["@cloudflare/workers-types"], 9 | "skipDefaultLibCheck": true, 10 | "strictFunctionTypes": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "alwaysStrict": true, 15 | "noEmit": true, 16 | }, 17 | "include": [ 18 | "challenges/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/build/**" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------