diff --git a/db/migrations/20200830172615_initial.sql b/db/migrations/20200830172615_initial.sql index 9e5234a..e3bad95 100644 --- a/db/migrations/20200830172615_initial.sql +++ b/db/migrations/20200830172615_initial.sql @@ -101,6 +101,8 @@ WITH stackcoin_reserve_system_user AS ( ) INSERT INTO "internal_user" SELECT id, username AS identifier FROM stackcoin_reserve_system_user; +SELECT setval('"user_id_seq"', (SELECT MAX(id) FROM "user")); + COMMIT; -- +micrate Down diff --git a/docker-compose.yml b/docker-compose.yml index 7dca882..6b5aebb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - services: stackcoin: build: ./. @@ -8,31 +6,24 @@ services: - ./.env ports: - 127.0.0.1:3000:3000 - networks: - - backend - postgres: - image: postgres:12 + db: + image: postgres:17 env_file: - ./.env ports: - 127.0.0.1:5432:5432 - networks: - - backend volumes: - db:/var/lib/postgresql/data hasura: - image: hasura/graphql-engine:v2.0.3 + image: hasura/graphql-engine:v2 env_file: - ./.env ports: - 127.0.0.1:8080:8080 depends_on: - - postgres + - db volumes: db: - -networks: - backend: diff --git a/shard.lock b/shard.lock index 9702966..64ea12e 100644 --- a/shard.lock +++ b/shard.lock @@ -2,15 +2,15 @@ version: 2.0 shards: backtracer: git: https://github.com/sija/backtracer.cr.git - version: 1.2.1 + version: 1.2.2 db: git: https://github.com/crystal-lang/crystal-db.git - version: 0.10.1 + version: 0.11.0 discordcr: - git: https://github.com/shardlab/discordcr.git - version: 0.4.1 + git: https://github.com/soya-daizu/discordcr.git + version: 0.4.0+git.commit.a409ca150bc9a8fcd42297520a59ddd67ffaebce dotenv: git: https://github.com/gdotdesign/cr-dotenv.git @@ -18,41 +18,57 @@ shards: exception_page: git: https://github.com/crystal-loot/exception_page.git - version: 0.2.0 + version: 0.5.0 + + graphql: + git: https://github.com/graphql-crystal/graphql.git + version: 0.4.0 humanize_time: git: https://github.com/mamantoha/humanize_time.git - version: 0.10.1 + version: 0.12.2 i18n: git: https://github.com/crystal-i18n/i18n.git - version: 0.2.0 + version: 0.2.1 + + jbuilder: + git: https://github.com/shootingfly/jbuilder.git + version: 1.0.0 kemal: git: https://github.com/kemalcr/kemal.git - version: 1.0.0+git.commit.218be2422172d330feb62c6a8abc7df5402fdb84 + version: 1.6.0+git.commit.749c537e853af6f032b01cf2b91ae2e740340d62 - kilt: - git: https://github.com/jeromegn/kilt.git - version: 0.6.1 + logger: + git: https://github.com/crystal-lang/logger.cr.git + version: 0.1.0 micrate: git: https://github.com/amberframework/micrate.git - version: 0.11.0 + version: 0.15.1 mysql: git: https://github.com/crystal-lang/crystal-mysql.git - version: 0.13.0 + version: 0.14.0 pg: git: https://github.com/will/crystal-pg.git - version: 0.23.2 + version: 0.26.0 radix: git: https://github.com/luislavena/radix.git version: 0.4.1 + runcobo: + git: https://github.com/runcobo/runcobo.git + version: 2.0.0 + sqlite3: git: https://github.com/crystal-lang/crystal-sqlite3.git - version: 0.18.0 + version: 0.19.0 + + water: + git: https://github.com/shootingfly/water.git + version: 1.0.0 diff --git a/shard.yml b/shard.yml index c35e038..c74be75 100644 --- a/shard.yml +++ b/shard.yml @@ -15,11 +15,14 @@ dependencies: pg: github: will/crystal-pg + logger: + github: crystal-lang/logger.cr + micrate: github: amberframework/micrate discordcr: - github: shardlab/discordcr + github: soya-daizu/discordcr dotenv: github: gdotdesign/cr-dotenv @@ -31,13 +34,11 @@ dependencies: github: kemalcr/kemal branch: master - #runcobo: - # github: runcobo/runcobo - sqlite3: github: crystal-lang/crystal-sqlite3 - #graphql: - # github: graphql-crystal/graphql + runcobo: + github: runcobo/runcobo -crystal: 1.0.0 + graphql: + github: graphql-crystal/graphql diff --git a/spec/stubs.cr b/spec/stubs.cr index bc93a01..6a04d3d 100644 --- a/spec/stubs.cr +++ b/spec/stubs.cr @@ -5,19 +5,19 @@ record MessageAuthor, id : Discord::Snowflake, username : String, avatar_url : S end end -record MessageStub, channel_id : Discord::Snowflake, guild_id : Discord::Snowflake, content : String, author : MessageAuthor do +record MessageStub, channel_id : Discord::Snowflake, guild_id : Discord::Snowflake, content : String, author : MessageAuthor, message_reference : Discord::MessageReference? do def self.new(channel_id, guild_id, content, author) channel_id = Discord::Snowflake.new(channel_id.to_u64) guild_id = Discord::Snowflake.new(guild_id.to_u64) - new(channel_id, guild_id, content, author) + new(channel_id, guild_id, content, author, nil) end end -record MessageWithEmbedStub, channel_id : Discord::Snowflake, guild_id : Discord::Snowflake, content : String, author : MessageAuthor, embed : Discord::Embed do +record MessageWithEmbedStub, channel_id : Discord::Snowflake, guild_id : Discord::Snowflake, content : String, author : MessageAuthor, embed : Discord::Embed, message_reference : Discord::MessageReference? do def self.new(channel_id, guild_id, content, author, embed) channel_id = Discord::Snowflake.new(channel_id.to_u64) guild_id = Discord::Snowflake.new(guild_id.to_u64) - new(channel_id, guild_id, content, author, embed) + new(channel_id, guild_id, content, author, embed, nil) end end @@ -33,12 +33,12 @@ class MockClient # TODO class_property current_guild = CSBOIS_GUILD_SNOWFLAKE - def create_message(channel_id : Discord::Snowflake, content : String) - # TODO MessageStub.new(channel_id, @@current_guild, content) + def create_message(channel_id : Discord::Snowflake, content : String, message_reference : Discord::MessageReference? = nil) + # TODO MessageStub.new(channel_id, @@current_guild, content, message_reference) end - def create_message(channel_id : Discord::Snowflake, content : String, embed : Discord::Embed) - # TODO MessageWithEmbedStub.new(channel_id, @@current_guild, content, embed) + def create_message(channel_id : Discord::Snowflake, content : String, embed : Discord::Embed, message_reference : Discord::MessageReference? = nil) + # TODO MessageWithEmbedStub.new(channel_id, @@current_guild, content, embed, message_reference) end def create_dm(user_id : Discord::Snowflake) diff --git a/src/cli.cr b/src/cli.cr index 6f6823f..70e0388 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -38,11 +38,10 @@ parser = OptionParser.parse do |parser| exit end - # TODO bringb back api - # parser.on("-s", "--schema", "Print the schema of the internal GraphQL Api") do - # puts StackCoin::Api::Internal::Gql.schema.document.to_s - # exit - # end + parser.on("-s", "--schema", "Print the schema of the internal GraphQL Api") do + puts StackCoin::Api::Internal::Gql.schema.document.to_s + exit + end parser.on("-h", "--help", "Show this help") do puts parser diff --git a/src/stackcoin.cr b/src/stackcoin.cr index b3f4544..895c416 100644 --- a/src/stackcoin.cr +++ b/src/stackcoin.cr @@ -2,19 +2,18 @@ require "./stackcoin/config" require "./stackcoin/db" require "./stackcoin/core" require "./stackcoin/bot" - -# TODO bring back api -# require "./stackcoin/api" +require "./stackcoin/api" module StackCoin + TMP_DIR = "/tmp/stackcoin/" + def self.run! - Dir.mkdir_p("/tmp/stackcoin/") + Dir.mkdir_p(TMP_DIR) run_migrations - # TODO bring back api - # spawn(Api::External.run!) - # spawn(Api::Internal.run!) + spawn(Api::External.run!) + spawn(Api::Internal.run!) spawn(Bot.run!) diff --git a/src/stackcoin/api/external.cr b/src/stackcoin/api/external.cr index d57c8e6..e038060 100644 --- a/src/stackcoin/api/external.cr +++ b/src/stackcoin/api/external.cr @@ -18,7 +18,11 @@ class StackCoin::Api::External::Auth < BaseAction cookie = Core::SessionStore::Session.to_cookie(result.new_session_id) context.response.cookies << cookie - render_plain("TODO redirect") # TODO redirect + context = render_plain("redirecting") + + context.response.status_code = 303 + context.response.headers["Location"] = "/" + context else render_plain(result.message) end @@ -33,6 +37,55 @@ class StackCoin::Api::External::Default < BaseAction get "/*" call do |context| - render_plain("...") + context = render_plain(<<-HTML + + + + + + + + +

+ +

+
+
+

stackcoin

+

this website has no functionality, at the moment

+
+ + + HTML + ) + + context.response.content_type = "text/html" + context end end diff --git a/src/stackcoin/api/internal.cr b/src/stackcoin/api/internal.cr index 0e50654..ef91d89 100644 --- a/src/stackcoin/api/internal.cr +++ b/src/stackcoin/api/internal.cr @@ -1,4 +1,5 @@ require "http/server" +require "log" class StackCoin::Api::Internal end @@ -6,10 +7,20 @@ end require "./internal/*" class StackCoin::Api::Internal - def self.not_found(r) - r.status_code = 404 + Log = ::Log.for("stackcoin.api.internal") + + def self.basic_message(r, message) + r.status_code = 200 r.content_type = "text/plain" - r.print("Not found") + r.print(message) + end + + def self.not_found(r) + basic_message(r, "Not found") + end + + def self.invalid_method(r) + basic_message(r, "Invalid method") end class SchemaExecuteInput @@ -32,7 +43,7 @@ class StackCoin::Api::Internal case resource when "/auth" unless method == "GET" - next not_found(r) + next invalid_method(r) end if token = context.request.headers["Authorization"]? @@ -59,7 +70,7 @@ class StackCoin::Api::Internal next when "/graphql" unless method == "POST" - next not_found(r) + next invalid_method(r) end headers = context.request.headers @@ -92,6 +103,7 @@ class StackCoin::Api::Internal end address = server.bind_tcp(4000) + Log.info { "Listening on #{address}" } server.listen end end diff --git a/src/stackcoin/api/internal/graphql.cr b/src/stackcoin/api/internal/graphql.cr index dba49b8..79b1a32 100644 --- a/src/stackcoin/api/internal/graphql.cr +++ b/src/stackcoin/api/internal/graphql.cr @@ -61,8 +61,8 @@ class StackCoin::Api::Internal::Gql include GraphQL::QueryType @[GraphQL::Field] - def pid : Int64 - Process.pid + def pid : String + Process.pid.to_s end end diff --git a/src/stackcoin/bot.cr b/src/stackcoin/bot.cr index d128c7f..ba08124 100644 --- a/src/stackcoin/bot.cr +++ b/src/stackcoin/bot.cr @@ -37,8 +37,7 @@ class StackCoin::Bot Commands::Dole.new, Commands::Graph.new, Commands::Leaderboard.new, - # TODO bring back when api is ready - # Commands::Login.new, + Commands::Login.new, Commands::Mark.new, Commands::Open.new, Commands::Profile.new, @@ -85,7 +84,11 @@ class StackCoin::Bot end def send_message(message, content) - @client.create_message(message.channel_id, content) + @client.create_message( + message.channel_id, + content, + message_reference: message.message_reference, + ) end def handle_message(message) @@ -94,7 +97,7 @@ class StackCoin::Bot return if parsed.nil? valid_check = Core::Group.validate_group_channel(message.guild_id, message.channel_id) - unless valid_check.is_a?(Core::Group::Result::ValidGroupChannel) + unless valid_check.is_a?(Core::Group::Result::ValidChannel) || parsed.command == "mark" send_message(message, valid_check.message) return end diff --git a/src/stackcoin/bot/command.cr b/src/stackcoin/bot/command.cr index ce992b3..640afdc 100644 --- a/src/stackcoin/bot/command.cr +++ b/src/stackcoin/bot/command.cr @@ -25,7 +25,11 @@ class StackCoin::Bot end def send_message(message, content) - client.create_message(message.channel_id, content) + client.create_message( + message.channel_id, + content, + message_reference: message.message_reference, + ) end def send_embed(message, emb : Discord::Embed) @@ -39,7 +43,12 @@ class StackCoin::Bot text: "StackCoin™", icon_url: "https://i.imgur.com/CsVxtvM.png" ) - client.create_message(message.channel_id, content, emb) + client.create_message( + message.channel_id, + content, + emb, + message_reference: message.message_reference, + ) end end end diff --git a/src/stackcoin/bot/commands/login.cr b/src/stackcoin/bot/commands/login.cr index f962a9b..75b4e61 100644 --- a/src/stackcoin/bot/commands/login.cr +++ b/src/stackcoin/bot/commands/login.cr @@ -27,7 +27,10 @@ class StackCoin::Bot::Commands Here's your one time login link: #{result.link} MESSAGE - send_message(message, "One time login link sent to you, check your direct messages with this bot") + + unless message.guild_id.nil? + send_message(message, "One time login link sent to you, check your direct messages with this bot") + end rescue Discord::CodeException send_message(message, "Failed to send your a direct message, cannot send one time link via Discord") end diff --git a/src/stackcoin/bot/commands/mark.cr b/src/stackcoin/bot/commands/mark.cr index b4de109..912a12a 100644 --- a/src/stackcoin/bot/commands/mark.cr +++ b/src/stackcoin/bot/commands/mark.cr @@ -12,7 +12,23 @@ class StackCoin::Bot::Commands raise Parser::Error.new("Expected no arguments, got #{parsed.arguments.size}") end - send_message(message, "TODO") + guild_id = message.guild_id + + unless guild_id.is_a?(Discord::Snowflake) + raise Parser::Error.new("Cannot invoke command in DMs") + end + + result = nil + DB.transaction do |tx| + cnn = tx.connection + invokee_id = user_id_from_snowflake(cnn, message.author.id) + result = Core::Group.set_group_channel(tx, invokee_id, guild_id, message.channel_id) + end + result = result.as(Result::Base) + + send_message(message, result.message) + + result end end end diff --git a/src/stackcoin/core/banned.cr b/src/stackcoin/core/banned.cr index ac3342b..cd26879 100644 --- a/src/stackcoin/core/banned.cr +++ b/src/stackcoin/core/banned.cr @@ -21,7 +21,7 @@ class StackCoin::Core::Banned def self.ban(tx : ::DB::Transaction, invokee_id : Int32?, user_id : Int32?) : Result::Base unless invokee_id.is_a?(Int32) - return Result::NoSuchUserAccount.new("You doesn't have a user account") + return Result::NoSuchUserAccount.new("You don't have a user account") end unless user_id.is_a?(Int32) @@ -55,7 +55,7 @@ class StackCoin::Core::Banned def self.unban(tx : ::DB::Transaction, invokee_id : Int32?, user_id : Int32?) : Result::Base unless invokee_id.is_a?(Int32) - return Result::NoSuchUserAccount.new("You doesn't have a user account") + return Result::NoSuchUserAccount.new("You don't have a user account") end unless user_id.is_a?(Int32) diff --git a/src/stackcoin/core/graph.cr b/src/stackcoin/core/graph.cr index ae33d01..dadf770 100644 --- a/src/stackcoin/core/graph.cr +++ b/src/stackcoin/core/graph.cr @@ -54,7 +54,7 @@ class StackCoin::Core::Graph end random = UUID.random - image_filename = "/tmp/stackcoin/graph_#{user_id}_#{random}.png" + image_filename = "#{StackCoin::TMP_DIR}/graph_#{user_id}_#{random}.png" title = "User ##{user_id} - #{Time.utc}" process = Process.new( diff --git a/src/stackcoin/core/group.cr b/src/stackcoin/core/group.cr index 90ea773..193b196 100644 --- a/src/stackcoin/core/group.cr +++ b/src/stackcoin/core/group.cr @@ -15,7 +15,13 @@ class StackCoin::Core::Group class NotAuthorized < Failure end - class ValidGroupChannel < Success + class ValidChannel < Success + end + + class ValidGroupChannel < ValidChannel + end + + class ValidDirectMessage < ValidChannel end end @@ -31,7 +37,7 @@ class StackCoin::Core::Group def self.validate_group_channel(guild_id : Discord::Snowflake?, channel_id : Discord::Snowflake) : Result::Base unless guild_id.is_a?(Discord::Snowflake) - return Result::NoDirectMessage.new("Can't access via direct message") + return Result::ValidDirectMessage.new("Valid direct message") end designated_channel = if discord_guild_to_channel_cache.includes?(guild_id) @@ -51,9 +57,14 @@ class StackCoin::Core::Group Result::ValidGroupChannel.new("Valid group channel") end - def self.set_group_channel(tx : ::DB::Transaction, invokee_id : Int32?, guild_id : Discord::Snowflake, channel_id : Discord::Snowflake?) + def self.set_group_channel( + tx : ::DB::Transaction, + invokee_id : Int32?, + guild_id : Discord::Snowflake, + channel_id : Discord::Snowflake + ) unless invokee_id.is_a?(Int32) - return Result::NoSuchUserAccount.new("You doesn't have a user account") + return Result::NoSuchUserAccount.new("You don't have a user account") end cnn = tx.connection @@ -62,8 +73,29 @@ class StackCoin::Core::Group SELECT admin FROM "user" WHERE id = $1 SQL - unless chanel_id - # TODO remove + unless invokee_is_admin + return Result::NotAuthorized.new("Not authorized to set the group channel") end + + discord_guild_exists = cnn.query_one(<<-SQL, guild_id, as: Bool) + SELECT EXISTS(SELECT 1 FROM "discord_guild" WHERE snowflake = $1) + SQL + + unless discord_guild_exists + guild = Bot::INSTANCE.cache.resolve_guild(guild_id) + cnn.exec(<<-SQL, guild_id, guild.name, guild.icon_url, channel_id, Time.utc) + INSERT INTO "discord_guild" ( + snowflake, name, icon_url, designated_channel_snowflake, last_updated + ) VALUES ( + $1, $2, $3, $4, $5 + ) + SQL + end + + cnn.exec(<<-SQL, channel_id, guild_id) + UPDATE "discord_guild" SET designated_channel_snowflake = $1 WHERE snowflake = $2 + SQL + + Result::Success.new("Channel set to <##{channel_id}>") end end diff --git a/src/stackcoin/core/session_store.cr b/src/stackcoin/core/session_store.cr index 7403404..58f1c55 100644 --- a/src/stackcoin/core/session_store.cr +++ b/src/stackcoin/core/session_store.cr @@ -33,7 +33,8 @@ class StackCoin::Core::SessionStore end def self.one_time_link(id : String) - URI.encode("#{STACKCOIN_SITE_BASE}/auth?one_time_key=#{id}") + encoded_id = URI.encode_path(id) + "#{STACKCOIN_SITE_BASE}/auth?one_time_key=#{id}" end def self.to_cookie(id : String) @@ -79,11 +80,11 @@ class StackCoin::Core::SessionStore end private def self.is_session_still_valid(session : Session) : Bool - session.expires_at < Time.utc + session.expires_at > Time.utc end private def self.is_session_still_valid(session_key : String) : Bool - if session = in_memory_session_store[one_time_key]? + if session = in_memory_session_store[session_key]? is_session_still_valid(session) else false diff --git a/src/stackcoin/core/stackcoin_reserve_system.cr b/src/stackcoin/core/stackcoin_reserve_system.cr index 235ea14..2fecaed 100644 --- a/src/stackcoin/core/stackcoin_reserve_system.cr +++ b/src/stackcoin/core/stackcoin_reserve_system.cr @@ -96,12 +96,14 @@ class StackCoin::Core::StackCoinReserveSystem time, label ) VALUES ( - $1, $2, $3, $4, $5, $5 + $1, $2, $3, $4, $5, $6 ) RETURNING id SQL + new_balance = Bank.balance(cnn, user_id).as(Bank::Result::Balance).balance + return Result::Pump.new( - "Successfully pumped the StackCoin Reserve System with #{amount} STK, with label: \"#{label}\"", + "Successfully pumped the StackCoin Reserve System with #{amount} STK, with label: \"#{label}\", the new amount of STK in the reserve is #{new_balance} STK", pump_id: pump_id, stackcoin_reserve_system_user_balance: new_balance, ) diff --git a/the-migration/the-migration.cr b/the-migration/the-migration.cr index 262d574..2edb877 100644 --- a/the-migration/the-migration.cr +++ b/the-migration/the-migration.cr @@ -552,6 +552,35 @@ new_db.transaction do |tx| new_transactions.each do |new_transaction| new_transaction.insert(cnn) end + + cnn.exec(<<-SQL + DO $$ + DECLARE + seq RECORD; + tables_to_fix text[] := ARRAY['user', 'discord_guild', 'transaction', 'pump', 'request']; + BEGIN + FOR seq IN + SELECT + table_name, + column_name, + pg_get_serial_sequence(format('%I.%I', table_schema, table_name), column_name) AS seq_name + FROM + information_schema.columns + WHERE + column_default LIKE 'nextval(%' + AND table_name = ANY(tables_to_fix) + LOOP + EXECUTE format( + 'SELECT setval(%L, COALESCE(MAX(%I), 0) + 1, false) FROM %I.%I', + seq.seq_name, + seq.column_name, + 'public', + seq.table_name + ); + END LOOP; + END $$; + SQL + ) end puts "inserted things into the database"