diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..bfc4c90 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,10 @@ +# ignore everything +* + +# except the following +!.gcloudignore +!.main.py +!.requirements.txt +!telegram_bot/bot.py +!telegram_bot/menu.py +!telegram_bot/init.py \ No newline at end of file diff --git a/Pipfile b/Pipfile index 0ce813b..351f595 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ python-dotenv = "*" python-telegram-bot = "*" pytz = "*" functions-framework = "*" +pulumi-random = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b5efe36..70bb6d6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6b3c4289bbf92fcb79325b7fa45280eb28120f3dba79d1e79ee0d957b1c9d77b" + "sha256": "9328f1813d374fe25e5d07d0d2b1c2ec8a671299b9c2262a391564e9a26ec4d0" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,21 @@ "markers": "python_version >= '3.9'", "version": "==4.9.0" }, + "arpeggio": { + "hashes": [ + "sha256:c790b2b06e226d2dd468e4fbfb5b7f506cec66416031fde1441cf1de2a0ba700", + "sha256:f7c8ae4f4056a89e020c24c7202ac8df3e2bc84e416746f20b0da35bb1de0250" + ], + "version": "==2.0.2" + }, + "attrs": { + "hashes": [ + "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", + "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + ], + "markers": "python_version >= '3.8'", + "version": "==25.3.0" + }, "beautifulsoup4": { "hashes": [ "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", @@ -169,6 +184,38 @@ ], "version": "==1.11.0" }, + "debugpy": { + "hashes": [ + "sha256:135ccd2b1161bade72a7a099c9208811c137a150839e970aeaf121c2467debe8", + "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e", + "sha256:211238306331a9089e253fd997213bc4a4c65f949271057d6695953254095376", + "sha256:2801329c38f77c47976d341d18040a9ac09d0c71bf2c8b484ad27c74f83dc36f", + "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65", + "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870", + "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea", + "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a", + "sha256:687c7ab47948697c03b8f81424aa6dc3f923e6ebab1294732df1ca9773cc67bc", + "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a", + "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121", + "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508", + "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787", + "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6", + "sha256:88eb9ffdfb59bf63835d146c183d6dba1f722b3ae2a5f4b9fc03e925b3358922", + "sha256:a2ba6fc5d7c4bc84bcae6c5f8edf5988146e55ae654b1bb36fecee9e5e77e9e2", + "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4", + "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898", + "sha256:b5aea1083f6f50023e8509399d7dc6535a351cc9f2e8827d1e093175e4d9fa4c", + "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b", + "sha256:c2c47c2e52b40449552843b913786499efcc3dbc21d6c49287d939cd0dbc49fd", + "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a", + "sha256:d58c48d8dbbbf48a3a3a638714a2d16de537b0dace1e3432b8e92c57d43707f8", + "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378", + "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493", + "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817" + ], + "markers": "python_version >= '3.8'", + "version": "==1.8.16" + }, "deprecation": { "hashes": [ "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", @@ -176,6 +223,14 @@ ], "version": "==2.1.0" }, + "dill": { + "hashes": [ + "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", + "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" + ], + "markers": "python_version >= '3.8'", + "version": "==0.4.0" + }, "flask": { "hashes": [ "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", @@ -193,6 +248,67 @@ "markers": "python_version >= '3.5' and python_version < '4'", "version": "==3.9.2" }, + "grpcio": { + "hashes": [ + "sha256:02697eb4a5cbe5a9639f57323b4c37bcb3ab2d48cec5da3dc2f13334d72790dd", + "sha256:03b0b307ba26fae695e067b94cbb014e27390f8bc5ac7a3a39b7723fed085604", + "sha256:05bc2ceadc2529ab0b227b1310d249d95d9001cd106aa4d31e8871ad3c428d73", + "sha256:06de8ec0bd71be123eec15b0e0d457474931c2c407869b6c349bd9bed4adbac3", + "sha256:0be4e0490c28da5377283861bed2941d1d20ec017ca397a5df4394d1c31a9b50", + "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", + "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", + "sha256:2018b053aa15782db2541ca01a7edb56a0bf18c77efed975392583725974b249", + "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75", + "sha256:2335c58560a9e92ac58ff2bc5649952f9b37d0735608242973c7a8b94a6437d8", + "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", + "sha256:38b68498ff579a3b1ee8f93a05eb48dc2595795f2f62716e797dc24774c1aaa8", + "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", + "sha256:3ed71e81782966ffead60268bbda31ea3f725ebf8aa73634d5dda44f2cf3fb9c", + "sha256:45a3d462826f4868b442a6b8fdbe8b87b45eb4f5b5308168c156b21eca43f61c", + "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", + "sha256:4e504572433f4e72b12394977679161d495c4c9581ba34a88d843eaf0f2fbd39", + "sha256:4ea1d062c9230278793820146c95d038dc0f468cbdd172eec3363e42ff1c7d01", + "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", + "sha256:6001e575b8bbd89eee11960bb640b6da6ae110cf08113a075f1e2051cc596cae", + "sha256:66a0cd8ba6512b401d7ed46bb03f4ee455839957f28b8d61e7708056a806ba6a", + "sha256:6851de821249340bdb100df5eacfecfc4e6075fa85c6df7ee0eb213170ec8e5d", + "sha256:728bdf36a186e7f51da73be7f8d09457a03061be848718d0edf000e709418987", + "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a", + "sha256:73fc8f8b9b5c4a03e802b3cd0c18b2b06b410d3c1dcbef989fdeb943bd44aff7", + "sha256:78fa51ebc2d9242c0fc5db0feecc57a9943303b46664ad89921f5079e2e4ada7", + "sha256:7b2c86457145ce14c38e5bf6bdc19ef88e66c5fee2c3d83285c5aef026ba93b3", + "sha256:7d69ce1f324dc2d71e40c9261d3fdbe7d4c9d60f332069ff9b2a4d8a257c7b2b", + "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", + "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", + "sha256:8ac475e8da31484efa25abb774674d837b343afb78bb3bcdef10f81a93e3d6bf", + "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", + "sha256:99a641995a6bc4287a6315989ee591ff58507aa1cbe4c2e70d88411c4dcc0839", + "sha256:9c3a99c519f4638e700e9e3f83952e27e2ea10873eecd7935823dab0c1c9250e", + "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b", + "sha256:a18e20d8321c6400185b4263e27982488cb5cdd62da69147087a76a24ef4e7e3", + "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", + "sha256:a9539f01cb04950fd4b5ab458e64a15f84c2acc273670072abe49a3f29bbad54", + "sha256:ad2efdbe90c73b0434cbe64ed372e12414ad03c06262279b104a029d1889d13e", + "sha256:b672abf90a964bfde2d0ecbce30f2329a47498ba75ce6f4da35a2f4532b7acbc", + "sha256:bbd27c24a4cc5e195a7f56cfd9312e366d5d61b86e36d46bbe538457ea6eb8dd", + "sha256:c400ba5675b67025c8a9f48aa846f12a39cf0c44df5cd060e23fda5b30e9359d", + "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", + "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7", + "sha256:ce89f5876662f146d4c1f695dda29d4433a5d01c8681fbd2539afff535da14d4", + "sha256:d25a14af966438cddf498b2e338f88d1c9706f3493b1d73b93f695c99c5f0e2a", + "sha256:d8d4732cc5052e92cea2f78b233c2e2a52998ac40cd651f40e398893ad0d06ec", + "sha256:d9a9724a156c8ec6a379869b23ba3323b7ea3600851c91489b871e375f710bc8", + "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", + "sha256:e88264caad6d8d00e7913996030bac8ad5f26b7411495848cc218bd3a9040b6c", + "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46", + "sha256:fb57870449dfcfac428afbb5a877829fcb0d6db9d9baa1148705739e9083880e", + "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf", + "sha256:fe96281713168a3270878255983d2cb1a97e034325c8c2c25169a69289d3ecfa", + "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679" + ], + "markers": "python_version >= '3.8'", + "version": "==1.66.2" + }, "gunicorn": { "hashes": [ "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", @@ -324,6 +440,55 @@ "markers": "python_version >= '3.8'", "version": "==25.0" }, + "parver": { + "hashes": [ + "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2", + "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777" + ], + "markers": "python_version >= '3.8'", + "version": "==0.5" + }, + "pip": { + "hashes": [ + "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", + "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717" + ], + "markers": "python_version >= '3.9'", + "version": "==25.2" + }, + "protobuf": { + "hashes": [ + "sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5", + "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", + "sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af", + "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", + "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", + "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", + "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", + "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", + "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", + "sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3", + "sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24" + ], + "markers": "python_version >= '3.8'", + "version": "==4.25.8" + }, + "pulumi": { + "hashes": [ + "sha256:8f3ae0f23f077c9578e7bc3814b6c65cb7166bd3daa0a644513b80cb0e834e48" + ], + "markers": "python_version >= '3.9'", + "version": "==3.188.0" + }, + "pulumi-random": { + "hashes": [ + "sha256:99e121d79aec547287ed36769a36ae76f4cb0b47ad027d289018abbde3507579", + "sha256:c980a7d32b3b8f814545aa52ff49cae2640f9b4c47b10bf7ad748a17a0b85e58" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==4.18.3" + }, "python-dotenv": { "hashes": [ "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", @@ -350,6 +515,65 @@ "index": "pypi", "version": "==2025.2" }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, "requests": { "hashes": [ "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", @@ -359,6 +583,14 @@ "markers": "python_version >= '3.8'", "version": "==2.32.4" }, + "semver": { + "hashes": [ + "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", + "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602" + ], + "markers": "python_version >= '3.7'", + "version": "==3.0.4" + }, "sniffio": { "hashes": [ "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", diff --git a/README.md b/README.md index c9a1bcb..26d8eb2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -SatO Telegram Bot -================= +# SatO Telegram Bot This bot has some useful functionality for Satakuntalainen Osakunta. It can be found on Telegram as `@osakuntabot`. @@ -10,9 +9,27 @@ Available commands: /ruokalista /tjviisi +## Development -Development ------------ +⚠️⚠️ Read the entirety of this section before making changes to the repository ⚠️⚠️ + +### Development flow + +1. Always create a new branch for whatever you are working on +2. When you're done, create a pull request +3. Check the preview for what changes will be done upon deployment +4. If everything looks good, merge the pull request and the changes will be automatically done + +### Warnings + +The code in the `master` branch is automatically deployed. Therefore you should be careful as to what you merge to this branch. + +Changes to the `infra/` directory modify the resources that are deployed on the cloud. These can incur extra cost if you are not careful. +Changes to the application source code (`main.py`, `telegram_bot/`) can also incur costs if they run for long or consume a lot of resources (processor time, bandwidth) + +Be mindful to the changes you make. You can ask for help and comments on your pull request before merging. After large changes, monitor the costs incurred manually on the cloud dashboard. + +### Local development Get `pipenv` from [here](https://pipenv.pypa.io/en/latest/) @@ -30,5 +47,4 @@ You can test the commands on commandline by: python main.py /command [args] -The bot is hosted in Google Cloud Functions and CircleCI is used to continuously deploy new versions of the bot to GCP. -NB! If you add dependencies to the project, remember to generate a new requirements.txt with `pipenv lock -r` and push it to the repo. Pipfile is not supported by Google Cloud Functions. +NB! If you add dependencies to the project, remember to generate a new requirements.txt with `pipenv requirements > requirements.txt` and push it to the repo. Pipfile is not supported by Google Cloud Functions. diff --git a/infra/Pulumi.prod.yaml b/infra/Pulumi.prod.yaml index cae52a9..5dd4553 100644 --- a/infra/Pulumi.prod.yaml +++ b/infra/Pulumi.prod.yaml @@ -1,3 +1,4 @@ -encryptionsalt: v1:MpvbSqwJ0R0=:v1:Gb5Aayq2PEDs+22I:lFM2LBSteNXhvO4PjNRCgZ5h783eDQ== +encryptionsalt: v1:dPeFWTLi9AI=:v1:oVtFIvCQGewIBERG:46y/iE1CtLrjeTkc2SilpwEjfJg0iw== config: - gcp:project: osakunta-telegram-bot + gcp:region: europe-north1 + gcp:disableGlobalProjectWarning: "true" diff --git a/infra/__main__.py b/infra/__main__.py index 54f9d34..5fd7e44 100644 --- a/infra/__main__.py +++ b/infra/__main__.py @@ -1,106 +1,248 @@ -"""A Python Pulumi program""" - import pulumi -import pulumi_gcp as gcp -import tarfile -import hashlib +import pulumi_gcp as gcp +import pulumi_random as random +import utils -# create .tar.gz source +gcp_config = pulumi.Config("gcp") +LOCATION = gcp_config.require("region") +STACK_NAME = pulumi.get_stack() -# SOURCE_TAR_NAME = "source.tar.gz" +project_id = random.RandomPet("project-id", + length=2 +) -# tarfile_hash = None -# with tarfile.open(SOURCE_TAR_NAME, "w|gz") as tar: -# tar.add("../telegram_bot", arcname="telegram_bot") -# tar.add("../main.py", arcname="main.py") -# tar.add("../requirements.txt", arcname="requirements.txt") -# last_modified = max([member.mtime for member in tar.getmembers()]) -# tarfile_hash = hashlib.sha256(str(last_modified).encode()).hexdigest() +project = gcp.organizations.Project("project", + name=f"Telegram Bot {STACK_NAME}", + project_id=project_id.id, + folder_id="452932952214" +) +# Enable required services / APIs +secretmanager_service = gcp.projects.Service("secretmanager-service", + project=project_id.id, + service="secretmanager.googleapis.com", + disable_on_destroy=True +) -# setup infrastructure +cloudbuild_service = gcp.projects.Service("cloudbuild-service", + project=project_id.id, + service="cloudbuild.googleapis.com", + disable_on_destroy=True +) -PROJECT_ID = "osakunta-telegram-bot" -LOCATION = "europe-north1" +cloudrun_service = gcp.projects.Service("cloudrun-service", + project=project_id.id, + service="run.googleapis.com", + disable_on_destroy=True +) +cloudfunctions_service = gcp.projects.Service("cloudfunctions-service", + project=project_id.id, + service="cloudfunctions.googleapis.com", + disable_on_destroy=True +) -# Set up secret to hold the Telegram API token -telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", - secret_id="telegram-bot-token", +cloudresourcemanager_service = gcp.projects.Service("cloudresourcemanager-service", + project=project_id.id, + service="cloudresourcemanager.googleapis.com", + disable_on_destroy=True +) + + +# --- Set up github repo connection --- +github_token_secret = gcp.secretmanager.Secret("github-token-secret", + project=project_id.id, + secret_id="github-token", replication={ "user_managed": { "replicas": [{ "location": LOCATION }] } - } + }, + opts=pulumi.ResourceOptions( + depends_on=[secretmanager_service] + ) ) -# Set up a service account that has access to the secret, for the Function to use -service_account = gcp.serviceaccount.Account("service-account", - account_id="telegram-bot-service-account", - display_name="Telegram Bot Service Account") - -secret_access = gcp.secretmanager.SecretIamMember("secret-access", - secret_id=telegram_bot_token.id, +github_connection_service_account_secret_access = gcp.secretmanager.SecretIamMember("github-connection-service-account-secret-access", + project=project_id.id, + secret_id=github_token_secret.id, role="roles/secretmanager.secretAccessor", - member=service_account.email.apply(lambda email: f"serviceAccount:{email}") + member=pulumi.Output.concat( + "serviceAccount:service-", + project.number, + "@gcp-sa-cloudbuild.iam.gserviceaccount.com" + ), + opts=pulumi.ResourceOptions( + depends_on=[cloudbuild_service] + ) ) +github_connection = gcp.cloudbuildv2.Connection("github-connection", + project=project_id.id, + name="github-connection", + location=LOCATION, + github_config={ + "app_installation_id": 30357801, + "authorizer_credential": { + "oauth_token_secret_version": github_token_secret.name.apply(lambda name: f"{name}/versions/latest") + } + }, + opts=pulumi.ResourceOptions( + depends_on=[github_connection_service_account_secret_access] + ) +) -# Set up the source code -source_bucket = gcp.storage.Bucket("source-bucket", +github_repository = gcp.cloudbuildv2.Repository("github-repository", + project=project_id.id, + name="telegram-bot", location=LOCATION, - name=f"{PROJECT_ID}-source-bucket", + parent_connection=github_connection.name, + remote_uri="https://github.com/osakunta/telegram-bot.git", ) -source_asset = pulumi.AssetArchive({ - "telegram_bot": pulumi.FileArchive("../telegram_bot"), - "main.py": pulumi.FileAsset("../main.py"), - "requirements.txt": pulumi.FileAsset("../requirements.txt") -}) -source_object = gcp.storage.BucketObject("source-object", - bucket=source_bucket.name, - name="telegram-bot-source", - source=source_asset +# --- Set up CI/CD --- + +cicd_service_account = utils.service_account_with_roles( + "cicd-service-account", + [ + "roles/logging.logWriter", + "roles/cloudfunctions.developer", + "roles/run.admin", + "roles/iam.serviceAccountUser", + "roles/storage.objectViewer", + "roles/artifactregistry.writer" + ], + project=project_id.id, + account_id="cicd-service-account", + display_name="CICD Service Account" ) -# Set up the Function, which handles the requests -function = gcp.cloudfunctionsv2.Function("function", - location=LOCATION, - name="telegram-bot-function", - description="Cloud Run Function for handling telegram bot requests", - build_config={ - "runtime": "python313", - "entryPoint": "telegram_bot", - "source": { - "storage_source": { - "bucket": source_bucket.name, - "object": source_object.name, - "generation": source_object.generation - } - } - }, - service_config={ - "availableMemory": "128Mi", - "maxInstanceCount": 1, # No need for more than one instance - "minInstanceCount": 0, # Important to allow scale-to-zero, to save costs - "service_account_email": service_account.email, - "ingressSettings": "ALLOW_ALL", - "secret_environment_variables": [{ - "key": "TOKEN", - "project_id": PROJECT_ID, - "secret": telegram_bot_token.secret_id, - "version": "latest" - }], - } +runtime_service_account = utils.service_account_with_roles( + "runtime-service-account", + [ "roles/iam.serviceAccountUser" ], + project=project_id.id, + account_id="runtime-service-account", + display_name="Function Runtime Service Account" ) -# Finally, set an IAM policy to allow unauthenticated people (anyone) to invoke the function -# this has to be cloudrun.ServiceIamMember instead of cloudfunctions.FunctionIamMember -# because the function is v2 -function_public_iam = gcp.cloudrunv2.ServiceIamMember("function-public-iam", - location=LOCATION, - name=function.name, - role="roles/run.invoker", - member="allUsers" +# used by the cloud function to access telegram API +telegram_api_token = utils.secret_with_access( + "telegram-api-token", + members=[runtime_service_account.member, cicd_service_account.member], + project=project_id.id, + secret_id="telegram-api-token", + replication={ + "user_managed": { + "replicas": [{ "location": LOCATION }] + } + }, + opts=pulumi.ResourceOptions( + depends_on=[secretmanager_service] + ) +) + +# telegram sends this alongside the updates to the bot, so that malicious actors cannot use the endpoint +telegram_webhook_token = utils.secret_with_access( + "telegram-webhook-token", + members=[runtime_service_account.member, cicd_service_account.member], + project=project_id.id, + secret_id="telegram-webhook-token", + replication={ + "user_managed": { + "replicas": [{ "location": LOCATION }] + } + }, + opts=pulumi.ResourceOptions( + depends_on=[secretmanager_service] + ) ) +deploy_trigger = gcp.cloudbuild.Trigger("deploy-trigger", + project=project_id.id, + name="deploy", + location=LOCATION, + service_account=cicd_service_account.id, + repository_event_config={ + "repository": github_repository.id, + "push": { + "branch": f"^{STACK_NAME}$", + } + }, + build={ + "steps": [ + { + "id": "Deploy function", + "name": "gcr.io/google.com/cloudsdktool/cloud-sdk:slim", + "entrypoint": "gcloud", + "args": [ + "functions", "deploy", "telegram-bot", + "--region", LOCATION, + "--runtime", "python313", + "--entry-point", "telegram_bot", + "--trigger-http", + "--allow-unauthenticated", + "--timeout", "5s", + "--gen2", + "--max-instances", "1", + "--min-instances", "0", + "--memory", "128Mi", + "--set-env-vars", "API_TOKEN=$$API_TOKEN,WEBHOOK_TOKEN=$$WEBHOOK_TOKEN", + "--clear-secrets", + "--source", ".", + "--run-service-account", runtime_service_account.email, + "--build-service-account", cicd_service_account.id, + ], + "secretEnv": [ + "API_TOKEN", + "WEBHOOK_TOKEN" + ] + }, + { + "id": "Get function URL", + "name": "gcr.io/google.com/cloudsdktool/cloud-sdk:slim", + "entrypoint": "bash", + "args": [ + "-c", + project_id.id.apply( + lambda id: + f"gcloud functions describe telegram-bot --region={LOCATION} --project={id} --format='value(url)' > /workspace/url.txt" + ) + ] + }, + { + "id": "Set webhook URL", + "name": "gcr.io/gcp-runtimes/ubuntu_20_0_4", + "entrypoint": "bash", + "args": [ + "-c", + "curl -X POST \ + -H \"Content-Type: application/json\" \ + -d \"{\\\"url\\\":\\\"$(cat /workspace/url.txt)\\\",\\\"secret_token\\\":\\\"$${WEBHOOK_TOKEN}\\\"}\" \ + \"https://api.telegram.org/bot$${API_TOKEN}/setWebhook\"" + ], + "secretEnv": [ + "API_TOKEN", + "WEBHOOK_TOKEN" + ] + } + ], + "options": { + "logging": "CLOUD_LOGGING_ONLY" + }, + "availableSecrets": { + "secretManager": [ + { + "env": "API_TOKEN", + "versionName": telegram_api_token.name.apply(lambda name: f"{name}/versions/latest"), + }, + { + "env": "WEBHOOK_TOKEN", + "versionName": telegram_webhook_token.name.apply(lambda name: f"{name}/versions/latest"), + } + ] + }, + }, + opts=pulumi.ResourceOptions( + depends_on=[ cloudrun_service, cloudfunctions_service, cloudresourcemanager_service ] + ) +) \ No newline at end of file diff --git a/infra/utils.py b/infra/utils.py new file mode 100644 index 0000000..e601992 --- /dev/null +++ b/infra/utils.py @@ -0,0 +1,32 @@ +import pulumi +import pulumi_gcp as gcp + +def service_account_with_roles(name, roles, **account_args): + account = gcp.serviceaccount.Account(name, **account_args) + for role in roles: + gcp.projects.IAMMember(f"{name}-role:{role}", + project=account.project, + role=role, + member=account.member, + opts=pulumi.ResourceOptions( + depends_on=[account] + ) + ) + return account + + +def secret_with_access(name, members, **secret_args): + secret = gcp.secretmanager.Secret(name, **secret_args) + for member in members: + member.apply( + lambda m: gcp.secretmanager.SecretIamMember(f"{name}-access:{m}", + project=secret.project, + secret_id=secret.id, + role="roles/secretmanager.secretAccessor", + member=m, + opts=pulumi.ResourceOptions( + depends_on=[secret] + ) + ) + ) + return secret \ No newline at end of file diff --git a/main.py b/main.py index 3faac09..e5ecce1 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,13 @@ def telegram_bot(request): level=logging.INFO ) - bot = telegram.Bot(token=os.getenv('TOKEN')) + # check the header for the secret token + secret_token = request.headers.get('X-Telegram-Bot-Api-Secret-Token') + if secret_token != os.getenv('WEBHOOK_TOKEN'): + logging.error("Invalid secret token") + return "Forbidden", 403 + + bot = telegram.Bot(token=os.getenv('API_TOKEN')) if request and request.method == "POST": try: @@ -31,8 +37,12 @@ def telegram_bot(request): command, args = parse_instructions(update) execute_bot_command(command, args, bot, update) + return "OK", 200 except Exception as e: logging.error(f"Error processing update: {e}") + return "Internal Server Error", 500 + + return "Invalid request", 400 # Used to test the bot on commandline by: python main.py /command [args] if __name__ == '__main__':