abstract class MainLayout
  include Lucky::HTMLPage
  include Shared::FieldErrorsComponent
  include Shared::FlashComponent

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User
  needs flash : Lucky::Flash::Store

  abstract def inner

  def render
    html_doctype

    html lang: "en" do
      head do
        utf8_charset
        title page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
      end

      body do
        render_flash
        inner
      end
    end
  end

  def page_title
    "Welcome to Lucky"
  end
end

abstract class MainLayout
  include Lucky::HTMLPage
  include Shared::FieldErrorsComponent
  include Shared::FlashComponent

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User
  needs flash : Lucky::Flash::Store

  abstract def inner

  def render
    html_doctype

    html lang: "en" do
      head do
        utf8_charset
        title page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
        csrf_meta_tags
      end

      body do
        render_flash
        inner
      end
    end
  end

  def page_title
    "Welcome to Lucky"
  end
end

Add CSRF meta tags to layout

Add CSRF meta tags to layout


require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      channel_id: {type: UInt64, converter: SnowflakeConverter},
      author: User,
      timestamp: {type: Time, converter: Time::Format::ISO_8601_DATE},
      tts: Bool,
      mention_everyone: Bool,
      mentions: Array(User)
    )
  end
end

require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8 | Nil,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      channel_id: {type: UInt64, converter: SnowflakeConverter},
      author: User,
      timestamp: {type: Time, converter: Time::Format::ISO_8601_DATE},
      tts: Bool,
      mention_everyone: Bool,
      mentions: Array(User)
    )
  end
end

Make the type field possibly be nil (apparently it is sometimes)


require "logger"

# Logger #######################################################################

LOG = Logger.new(STDOUT)
LOG.level = Logger::INFO
LOG.formatter = Logger::Formatter.new do |severity, _datetime, _progname, message, io|
  io << case severity
  when "DEBUG"
    ":: "
  when "INFO"
    ">> "
  when "WARN"
    "** "
  when "ERROR"
    "!! "
  when "FATAL"
    "   " # FIXME: Bullet for FATAL
  end
  io << message
  io << "..." if severity == "INFO"
end

# Main #########################################################################

LOG.info("Help!")

require "logger"
require "colorize"

# Logger #######################################################################

LOG = Logger.new(STDOUT)
LOG.level = Logger::INFO
LOG.formatter = Logger::Formatter.new do |severity, _datetime, _progname, message, io|
  io << case severity
  when "DEBUG"
    ":: ".colorize.white
  when "INFO"
    ">> ".colorize.green
  when "WARN"
    "** ".colorize.yellow
  when "ERROR"
    "!! ".colorize.red
  when "FATAL"
    "   " # FIXME: Bullet for FATAL
  end
  io << message
  io << "..." if severity == "INFO"
end

# Main #########################################################################

LOG.info("Help!")

Add colours to log messages

Add colours to log messages


module Optarg
  # :nodoc:
  abstract class ValueHash(V) < Hash(String, V)
    @fallbacked = {} of String => Bool
    @parser : Parser

    def initialize(@parser)
      super()
    end

    def [](key)
      fallback key
      super
    end

    def []?(key)
      fallback key
      super
    end

    def fallback(key)
      return if has_key?(key)
      return if @fallbacked.has_key?(key)
      @fallbacked[key] = true
      if fb = @parser.definitions.values[key]?
        fb.fallback_value @parser
      end
    end
  end
end

module Optarg
  # :nodoc:
  abstract class ValueHash(V)
    @raw = {} of String => V
    forward_missing_to @raw

    @fallbacked = {} of String => Bool
    @parser : Parser

    def initialize(@parser)
    end

    def ==(other : Hash)
      @raw == other
    end

    def [](key)
      fallback key
      @raw[key]
    end

    def []?(key)
      fallback key
      @raw[key]?
    end

    def fallback(key)
      return if @raw.has_key?(key)
      return if @fallbacked.has_key?(key)
      @fallbacked[key] = true
      if fb = @parser.definitions.values[key]?
        fb.fallback_value @parser
      end
    end
  end
end

Use Hash composition over inheritance in ValueHash


module Discord
  @[Flags]
  enum Permissions : UInt64
    CreateInstantInvite = 1
    KickMembers         = 1 << 1
    BanMembers          = 1 << 2
    Administrator       = 1 << 3
    ManageChannels      = 1 << 4
    ManageGuild         = 1 << 5
    ReadMessages        = 1 << 10
    SendMessages        = 1 << 11
    SendTTSMessages     = 1 << 12
    ManageMessages      = 1 << 13
    EmbedLinks          = 1 << 14
    AttachFiles         = 1 << 15
    ReadMessageHistory  = 1 << 16
    MentionEveryone     = 1 << 17
    UseExternalEmojis   = 1 << 18
    Connect             = 1 << 20
    Speak               = 1 << 21
    MuteMembers         = 1 << 22
    DeafenMembers       = 1 << 23
    MoveMembers         = 1 << 24
    UseVAD              = 1 << 25
    ChangeNickname      = 1 << 26
    ManageNicknames     = 1 << 27
    ManageRoles         = 1 << 28
    ManageWebhooks      = 1 << 29
    ManageEmojis        = 1 << 30

    def self.new(pull : JSON::PullParser)
      # see https://github.com/crystal-lang/crystal/issues/3448
      # #from_value errors
      Permissions.new(pull.read_int.to_u64)
    end
  end
end

module Discord
  @[Flags]
  enum Permissions : UInt64
    CreateInstantInvite = 1
    KickMembers         = 1 << 1
    BanMembers          = 1 << 2
    Administrator       = 1 << 3
    ManageChannels      = 1 << 4
    ManageGuild         = 1 << 5
    AddReactions        = 1 << 6
    ReadMessages        = 1 << 10
    SendMessages        = 1 << 11
    SendTTSMessages     = 1 << 12
    ManageMessages      = 1 << 13
    EmbedLinks          = 1 << 14
    AttachFiles         = 1 << 15
    ReadMessageHistory  = 1 << 16
    MentionEveryone     = 1 << 17
    UseExternalEmojis   = 1 << 18
    Connect             = 1 << 20
    Speak               = 1 << 21
    MuteMembers         = 1 << 22
    DeafenMembers       = 1 << 23
    MoveMembers         = 1 << 24
    UseVAD              = 1 << 25
    ChangeNickname      = 1 << 26
    ManageNicknames     = 1 << 27
    ManageRoles         = 1 << 28
    ManageWebhooks      = 1 << 29
    ManageEmojis        = 1 << 30

    def self.new(pull : JSON::PullParser)
      # see https://github.com/crystal-lang/crystal/issues/3448
      # #from_value errors
      Permissions.new(pull.read_int.to_u64)
    end
  end
end

Add AddReactions value (1<<6) to Permissions enum


require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "get"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      puts response.body

      GatewayResponse.from_json(response.body_io)
    end
  end
end

require "http/client"
require "openssl/ssl/context"

require "./mappings"

module Discordcr
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers | Nil, body : String | Nil)
      HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET = "GET"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      puts response.body

      GatewayResponse.from_json(response.body_io)
    end
  end
end

Use the correct method for the request

Use the correct method for the request


module Raven
  class Context
    # FIXME
    # @[ThreadLocal]
    @@current : self?

    def self.current
      @@current ||= new
    end

    def self.clear!
      @@current = nil
    end

    class_getter os_context : AnyHash::JSON do
      {
        name:           Raven.sys_command("uname -s"),
        version:        Raven.sys_command("uname -v"),
        build:          Raven.sys_command("uname -r"),
        kernel_version: Raven.sys_command("uname -a") || Raven.sys_command("ver"), # windows
      }.to_any_json
    end

    class_getter runtime_context : AnyHash::JSON do
      v = Raven.sys_command_compiled("crystal -v").match /^(.+?) (\d+.*)$/
      _, name, version = v.not_nil!
      {
        name:    name,
        version: version,
      }.to_any_json
    end

    any_json_property :contexts, :extra, :tags, :user

    def initialize
      @contexts = {
        os:      self.class.os_context,
        runtime: self.class.runtime_context,
      }.to_any_json
    end
  end
end

module Raven
  class Context
    # FIXME
    # @[ThreadLocal]
    @@current : self?

    def self.current
      @@current ||= new
    end

    def self.clear!
      @@current = nil
    end

    class_getter os_context : AnyHash::JSON do
      {
        name:           Raven.sys_command("uname -s"),
        version:        Raven.sys_command("uname -v"),
        build:          Raven.sys_command("uname -r"),
        kernel_version: Raven.sys_command("uname -a") || Raven.sys_command("ver"), # windows
      }.to_any_json
    end

    class_getter runtime_context : AnyHash::JSON do
      v = Crystal::DESCRIPTION.match /^(.+?) (\d+.*)$/
      _, name, version = v.not_nil!
      {
        name:    name,
        version: version,
      }.to_any_json
    end

    any_json_property :contexts, :extra, :tags, :user

    def initialize
      @contexts = {
        os:      self.class.os_context,
        runtime: self.class.runtime_context,
      }.to_any_json
    end
  end
end

Use `Crystal::DESCRIPTION` instead of spawning `crystal -v`

Moar efficient, yo


require "./converters"
require "./user"
require "./channel"
require "./guild"

module Discord
  module Gateway
    struct ReadyPayload
      JSON.mapping(
        v: UInt8,
        user: User,
        private_channels: Array(PrivateChannel),
        guilds: Array(UnavailableGuild),
        session_id: String
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end

    struct GuildDeletePayload
      JSON.mapping(
        id: {type: UInt64, converter: SnowflakeConverter},
        unavailable: {type: Bool, nilable: true}
      )
    end

    struct GuildBanPayload
      JSON.mapping(
        username: String,
        id: {type: UInt64, converter: SnowflakeConverter},
        discriminator: String,
        avatar: String,
        bot: {type: Bool, nilable: true},
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end

    struct GuildEmojiUpdatePayload
      JSON.mapping(
        guild_id: {type: UInt64, converter: SnowflakeConverter},
        emoji: {type: Array(Emoji), key: "emojis"}
      )
    end
  end
end

require "./converters"
require "./user"
require "./channel"
require "./guild"

module Discord
  module Gateway
    struct ReadyPayload
      JSON.mapping(
        v: UInt8,
        user: User,
        private_channels: Array(PrivateChannel),
        guilds: Array(UnavailableGuild),
        session_id: String
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end

    struct GuildDeletePayload
      JSON.mapping(
        id: {type: UInt64, converter: SnowflakeConverter},
        unavailable: {type: Bool, nilable: true}
      )
    end

    struct GuildBanPayload
      JSON.mapping(
        username: String,
        id: {type: UInt64, converter: SnowflakeConverter},
        discriminator: String,
        avatar: String,
        bot: {type: Bool, nilable: true},
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end

    struct GuildEmojiUpdatePayload
      JSON.mapping(
        guild_id: {type: UInt64, converter: SnowflakeConverter},
        emoji: {type: Array(Emoji), key: "emojis"}
      )
    end

    struct GuildIntegrationsUpdatePayload
      JSON.mapping(
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end
  end
end

Add a mapping for the GUILD_INTEGRATIONS_UPDATE dispatch payload


ch1 = Channel(String).new(16)

gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

spawn do
  gcfile.each_line() do |line|
    ch1.send(line)
  end
  ch1.close
end

at = 0
gc = 0

while line = ch1.receive
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |chr|
    case chr
    when 'A', 'T'
      at += 1
      next
    when 'G', 'C'
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)
puts "GC fraction: #{gcfrac}"

gcfile.close

ch1 = Channel(String).new(16)

gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

spawn do
  gcfile.each_line() do |line|
    ch1.send(line)
  end
  ch1.close
end

at = 0
gc = 0

while line = ch1.receive?
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |chr|
    case chr
    when 'A', 'T'
      at += 1
      next
    when 'G', 'C'
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)
puts "GC fraction: #{gcfrac}"

gcfile.close

Fix receive on closed chan error

Fix receive on closed chan error


require "any_hash"

require "./raven/ext/*"
require "./raven/mixins/*"
require "./raven/*"

module Raven
  # `Raven.instance` delegators.
  module Delegators
    delegate :context, :configuration, :client,
      :report_status, :configure, :send_feedback, :send_event,
      :capture, :last_event_id, :annotate_exception,
      :user_context, :tags_context, :extra_context, :breadcrumbs,
      to: :instance
  end
end

module Raven
  extend Delegators

  class_getter instance : Raven::Instance { Raven::Instance.new }

  macro sys_command_compiled(command)
    %result = {{ `(#{command.id} || true) 2>/dev/null`.stringify.strip }}
    %result unless %result.empty?
  end

  def self.sys_command(command)
    result = `(#{command}) 2>/dev/null`.strip rescue nil
    result if !result.nil? && !result.empty? && $?.success?
  end
end

require "any_hash"

require "./raven/ext/*"
require "./raven/mixins/*"
require "./raven/*"

module Raven
  # `Raven.instance` delegators.
  module Delegators
    delegate :context, :configuration, :client,
      :report_status, :configure, :send_feedback, :send_event,
      :capture, :last_event_id, :annotate_exception,
      :user_context, :tags_context, :extra_context, :breadcrumbs,
      to: :instance
  end
end

module Raven
  extend Delegators

  class_getter instance : Raven::Instance { Raven::Instance.new }

  macro sys_command_compiled(command)
    %result = {{ `(#{command.id} || true) 2>/dev/null`.stringify.strip }}
    %result.presence
  end

  def self.sys_command(command)
    result = `(#{command}) 2>/dev/null`.strip rescue nil
    result.presence if $?.success?
  end
end

Simplify logic just a bit

Simplify logic just a bit


abstract class LuckyCli::Task
  macro inherited
    LuckyCli::Runner.tasks << self.new

    def name
      "{{@type.name.gsub(/::/, ".").underscore}}"
    end
  end

  macro banner(banner_text)
    def banner
      {{banner_text}}
    end
  end

  # Set a custom title for the task
  #
  # By default the name is derived from the full module and class name.
  # However if that name is not desired, a custom one can be set here.
  # ```
  # class Dev::Prime < LuckyCli::Task
  #   name "Development database primer"
  #   banner "Seed the development database with example data"
  #
  #   # other methods, etc.
  # end
  # ```
  macro name(name_text)
    def name
      {{name_text}}
    end
  end

  abstract def call
  abstract def banner
end

abstract class LuckyCli::Task
  macro inherited
    LuckyCli::Runner.tasks << self.new

    def name
      "{{@type.name.gsub(/::/, ".").underscore}}"
    end
  end

  macro banner(banner_text)
    def banner
      {{banner_text}}
    end
  end

  # Sets a custom title for the task
  #
  # By default the name is derived from the full module and class name.
  # However if that name is not desired, a custom one can be set.
  #
  # ```
  # class Dev::Prime < LuckyCli::Task
  #   name "Development database primer"
  #   banner "Seed the development database with example data"
  #
  #   # other methods, etc.
  # end
  # ```
  macro name(name_text)
    def name
      {{name_text}}
    end
  end

  abstract def call
  abstract def banner
end

Add a newline to separate text from example code


require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = websocket = HTTP::WebSocket.new(URI.parse(url))
      websocket.on_message(&->on_message(String))
      websocket.run
    end

    private def on_message(message : String)
    end
  end
end

require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = websocket = HTTP::WebSocket.new(URI.parse(url))
      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    private def on_message(message : String)
    end
  end
end

Add a rudimentary close handler

Add a rudimentary close handler


require "./server"

Authentic.configure do
  settings.secret_key = Lucky::Server.settings.secret_key_base

  unless Lucky::Env.production?
    settings.encryption_cost = 4
  end
end

require "./server"

Authentic.configure do
  settings.secret_key = Lucky::Server.settings.secret_key_base

  unless Lucky::Env.production?
    fastest_encryption_possible = 4
    settings.encryption_cost = fastest_encryption_possible
  end
end

Clarify what '4' represents in encryption cost


require "./spec_helper"

struct StructWithSnowflake
  JSON.mapping(
    data: {type: UInt64, converter: Discord::SnowflakeConverter}
  )
end

describe Discord do
  describe Discord::SnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end
  end
end

require "./spec_helper"

struct StructWithSnowflake
  JSON.mapping(
    data: {type: UInt64, converter: Discord::SnowflakeConverter}
  )
end

struct StructWithMaybeSnowflake
  JSON.mapping(
    data: {type: UInt64 | Nil, converter: Discord::MaybeSnowflakeConverter}
  )
end

describe Discord do
  describe Discord::SnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end
  end

  describe Discord::MaybeSnowflakeConverter do
    it "converts a string to u64" do
      json = %({"data":"10000000000"})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq 10000000000
    end

    it "converts null to nil" do
      json = %({"data":null})

      obj = StructWithMaybeSnowflake.from_json(json)
      obj.data.should eq nil
    end
  end
end

Write a test for MaybeSnowflakeConverter


#require "./redis2ws/*"
require "kemal"
require "redis"

module Redis2ws
  SOCKETS = [] of HTTP::WebSocket
  puts "now here"
  get "/" do |ctx|
    send_file ctx, "src/web/index.html"
  end

  get "/app.js" do |ctx|
    send_file ctx, "src/web/app.js"
  end

  ws "/eventstream/" do |socket|
    # Add the client to SOCKETS list
    puts "Received new connection!"
    SOCKETS << socket

    # Broadcast each message to all clients
    socket.on_message do |message|
      # we don't really care about what clients send to us
      #SOCKETS.each { |socket| socket.send message}
    end

    # Remove clients from the list when it’s closed
    socket.on_close do
      SOCKETS.delete socket
    end
  end

  #run redis subscriber in its own fiber
  spawn do
    redis = Redis.new
    redis.subscribe("mychannel") do |on|
      on.message do |channel, message|
        puts message
        SOCKETS.each { |socket| socket.send message }
      end
    end
  end

  puts "past redis"
  Kemal.run
end

#require "./redis2ws/*"
require "kemal"
require "redis"

module Redis2ws
  SOCKETS = [] of HTTP::WebSocket

  #run redis subscriber in its own fiber
  spawn do
    redis = Redis.new
    redis.subscribe("mychannel") do |on|
      on.message do |channel, message|
        puts message
        SOCKETS.each { |socket| socket.send message }
      end
    end
  end

  # The Kemal app, pretty straightforward.

  # For root and /app.js we just hardcode some paths. You should probably use a nicer way for this.
  get "/" do |ctx|
    send_file ctx, "src/web/index.html"
  end

  get "/app.js" do |ctx|
    send_file ctx, "src/web/app.js"
  end

  ws "/eventstream/" do |socket|
    # Add the client to SOCKETS list
    SOCKETS << socket

    # Broadcast each message to all clients
    socket.on_message do |message|
      # we don't really care about what clients send to us, but leaving this in as an
      # example for how you could do stuff when people send you messages
    end

    # Remove clients from the list when it’s closed
    socket.on_close do
      SOCKETS.delete socket
    end
  end

  Kemal.run
end

Clean up code and comments

Clean up code and comments


require "ecr/macros"

module Artanis
  # TODO: render views in subpaths (eg: views/blog/posts/show.ecr => render_blog_posts_show_ecr)
  module Render
    macro ecr(name, layout = "layout")
      render {{ name }}, "ecr", layout: {{ layout }}
    end

    macro render(name, engine, layout = "layout")
      {% if layout %}
        render_{{ layout.id }}_{{ engine.id }} do
          render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
        end
      {% else %}
        render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
      {% end %}
    end

    macro views_path(path)
      {%
        views = `cd #{ path } 2>/dev/null && find . -name "*.ecr" -type f | cut -c 3- || true`
          .lines
          .map(&.strip.gsub(/\.ecr/, "").gsub(/\//, "__SLASH__"))
      %}

      {% for view in views %}
        def render_{{ view.id }}_ecr
          render_{{ view.id }}_ecr {}
        end

        def render_{{ view.id }}_ecr(&block)
          String.build do |__str__|
            embed_ecr "{{ path.id }}/{{ view.gsub(/__SLASH__/, "/").id }}.ecr", "__str__"
          end
        end
      {% end %}
    end
  end
end

require "ecr/macros"

module Artanis
  # TODO: render views in subpaths (eg: views/blog/posts/show.ecr => render_blog_posts_show_ecr)
  module Render
    macro ecr(name, layout = "layout")
      render {{ name }}, "ecr", layout: {{ layout }}
    end

    macro render(name, engine, layout = "layout")
      {% if layout %}
        render_{{ layout.id }}_{{ engine.id }} do
          render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
        end
      {% else %}
        render_{{ name.gsub(/\//, "__SLASH__").id }}_{{ engine.id }}
      {% end %}
    end

    macro views_path(path)
      {%
        views = `cd #{ path } 2>/dev/null && find . -name "*.ecr" -type f || true`
          .lines
          .map(&.strip
            .gsub(/^\.\//, "")
            .gsub(/\.ecr/, "")
            .gsub(/\//, "__SLASH__"))
      %}

      {% for view in views %}
        def render_{{ view.id }}_ecr
          render_{{ view.id }}_ecr {}
        end

        def render_{{ view.id }}_ecr(&block)
          String.build do |__str__|
            embed_ecr "{{ path.id }}/{{ view.gsub(/__SLASH__/, "/").id }}.ecr", "__str__"
          end
        end
      {% end %}
    end
  end
end

Replace cut command with Crystal code

Replace cut command with Crystal code


require "http"

class Kemal::CommonLogHandler < Kemal::BaseLogHandler
  @handler : IO::FileDescriptor
  getter handler

  def initialize(@env)
    @handler = if @env == "production"
                 handler = File.new("kemal.log", "a")
                 handler.flush_on_newline = true
                 handler
               else
                 STDOUT
               end
  end

  def call(context)
    time = Time.now
    call_next(context)
    elapsed = Time.now - time
    elapsed_text = elapsed_text(elapsed)
    write "#{time} #{context.response.status_code} #{context.request.method} #{context.request.resource} - #{elapsed_text}\n"
    context
  end

  private def elapsed_text(elapsed)
    minutes = elapsed.total_minutes
    return "#{minutes.round(2)}m" if minutes >= 1

    seconds = elapsed.total_seconds
    return "#{seconds.round(2)}s" if seconds >= 1

    millis = elapsed.total_milliseconds
    return "#{millis.round(2)}ms" if millis >= 1

    "#{(millis * 1000).round(2)}µs"
  end

  def write(message)
    if @env == "production"
      @handler.write message.to_slice
    else
      @handler.print message
    end
  end
end

require "http"

class Kemal::CommonLogHandler < Kemal::BaseLogHandler
  @handler : IO::FileDescriptor
  getter handler

  def initialize(@env)
    @handler = if @env == "production"
                 handler = File.new("kemal.log", "a")
                 handler.flush_on_newline = true
                 handler
               else
                 STDOUT
               end
  end

  def call(context)
    time = Time.now
    call_next(context)
    elapsed_text = elapsed_text(Time.now - time)
    @handler << time << " " << context.response.status_code << " " << context.request.method << " " << context.request.resource << " " << elapsed_text << "\n"
    @handler.rewind
    context
  end

  private def elapsed_text(elapsed)
    minutes = elapsed.total_minutes
    return "#{minutes.round(2)}m" if minutes >= 1

    seconds = elapsed.total_seconds
    return "#{seconds.round(2)}s" if seconds >= 1

    millis = elapsed.total_milliseconds
    return "#{millis.round(2)}ms" if millis >= 1

    "#{(millis * 1000).round(2)}µs"
  end
end

Optimize CommonLogHandler to directly use the handler instead of string interpolation


require "commander"
require "logger"

require "./crow.cr"

cli = Commander::Command.new do |cmd|
  cmd.use = "crow"
  cmd.long = "Transpile Crystal to Flow (JS) code."

  cmd.flags.add do |flag|
    flag.name = "verbose"
    flag.short = "-v"
    flag.long = "--verbose"
    flag.default = false
    flag.description = "Enable more verbose logging."
  end

  cmd.flags.add do |flag|
    flag.name = "no-strict"
    flag.long = "--no-strict"
    flag.default = false
    flag.description = "Disable strict mode (allow fallback transpilation)."
  end

  cmd.run do |options, arguments|
    Crow.logger = Logger.new(STDERR)
    Crow.strict = options.bool["strict"] && !options.bool["no-strict"]

    basename = nil
    input = if arguments.size == 0
              STDIN.gets_to_end
            else
              input_path = arguments[0]
              basename = File.basename(input_path).gsub(/\.[^.]+$/, "")

              File.read(input_path)
            end
    output = Crow.convert(input)

    if basename
      output_path = "#{basename}.js.flow"
      File.write(output_path, output)
    else
      STDOUT.puts output
    end
  end
end

Commander.run(cli, ARGV)

require "commander"
require "logger"

require "./crow.cr"

cli = Commander::Command.new do |cmd|
  cmd.use = "crow"
  cmd.long = "Transpile Crystal to Flow (JS) code."

  cmd.flags.add do |flag|
    flag.name = "version"
    flag.short = "-v"
    flag.long = "--version"
    flag.default = false
    flag.description = "Print version number and exit"
  end

  cmd.flags.add do |flag|
    flag.name = "no-strict"
    flag.long = "--no-strict"
    flag.default = false
    flag.description = "Disable strict mode (allow fallback transpilation)."
  end

  cmd.run do |options, arguments|
    if options.bool["version"]
      STDOUT.puts "Crow v#{Crow::VERSION}"
      next
    end

    Crow.logger = Logger.new(STDERR)
    Crow.strict = options.bool["strict"] && !options.bool["no-strict"]

    basename = nil
    input = if arguments.size == 0
              STDIN.gets_to_end
            else
              input_path = arguments[0]
              basename = File.basename(input_path).gsub(/\.[^.]+$/, "")

              File.read(input_path)
            end
    output = Crow.convert(input)

    if basename
      output_path = "#{basename}.js.flow"
      File.write(output_path, output)
    else
      STDOUT.puts output
    end
  end
end

Commander.run(cli, ARGV)

Replace -v flag with version in CLI

Replace -v flag with version in CLI


require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct PartialUser
    JSON.mapping(
      username: {type: String, nilable: true},
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: {type: String, nilable: true},
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end

require "./converters"

module Discord
  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )
  end

  struct PartialUser
    JSON.mapping(
      username: {type: String, nilable: true},
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: {type: String, nilable: true},
      avatar: {type: String, nilable: true},
      email: {type: String, nilable: true},
      bot: {type: Bool, nilable: true}
    )

    def full? : Bool
      !@username.nil? && !@discriminator.nil? && !@avatar.nil?
    end
  end

  struct UserGuild
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      icon: String,
      owner: Bool,
      permissions: UInt64
    )
  end

  struct Connection
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      name: String,
      type: String,
      revoked: Bool
    )
  end
end

Add a method to PartialUser to check whether it has all data

Required to check whether a presence update introduces a new member to the
cache.


require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_DISPATCH = 0
    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      when OP_DISPATCH
        handle_dispatch(json["t"], json["d"])
      else
        puts "Unsupported message: #{message}"
      end

      nil
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end

    private def handle_dispatch(type, data)
    end
  end
end

require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_DISPATCH = 0
    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      when OP_DISPATCH
        handle_dispatch(json["t"], json["d"])
      else
        puts "Unsupported message: #{message}"
      end

      nil
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end

    private def handle_dispatch(type, data)
      case type
      when "READY"
        puts "Received READY, v: #{data["v"]}"
      else
        puts "Unsupported dispatch: #{type} #{data}"
      end
    end
  end
end

Add basic READY handling to the dispatch handler


module Kemal::Exceptions
  class RouteNotFound < Exception
    def initialize(context)
      super "Requested path: '#{context.request.override_method as String}:#{context.request.path}' was not found."
    end
  end

  class CustomException < Exception
    getter context

    def initialize(@context)
      super "Rendered error with #{@context.response.status_code}"
    end
  end
end

module Kemal::Exceptions
  class RouteNotFound < Exception
    def initialize(context)
      super "Requested path: '#{context.request.override_method as String}:#{context.request.path}' was not found."
    end
  end

  class CustomException < Exception

    def initialize(context)
      super "Rendered error with #{context.response.status_code}"
    end
  end
end

Add instance types for Crystal 0.16.0

Add instance types for Crystal 0.16.0


require "json"

module Discordcr
  module Mappings
  end
end

require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    class GatewayResponse
      JSON.mapping (
        url: String
      )
    end
  end
end

Create a mapping for the gateway request response


require "colorize"
require "file_utils"
require "option_parser"
require "yaml"

require "./*"
require "./commands/*"

module Roadshow
  module CLI
    def self.run(stdin, stdout, args) : Int
      command = args.shift?

      if command
        Command.get_command(command).run(stdin, stdout, args)
      else
        print_usage(stdout)
      end

      0
    rescue UnknownCommand
      stdout.puts "Unknown command: #{command}".colorize(:red)
      print_usage(stdout)
      1
    rescue CommandFailed
      2
    end

    def self.print_usage(stdout)
      stdout.puts <<-HELP
      Usage: roadshow <command> [<options>]

      Commands:
         help        Get help about a command
         init        Generate a `scenarios.yml` file
      HELP
    end
  end
end

require "colorize"
require "file_utils"
require "option_parser"
require "yaml"

require "./*"
require "./commands/*"

module Roadshow
  module CLI
    def self.run(stdin, stdout, args) : Int
      command = args.shift?

      if command
        Command.get_command(command).run(stdin, stdout, args)
      else
        print_usage(stdout)
      end

      0
    rescue UnknownCommand
      stdout.puts "Unknown command: #{command}".colorize(:red)
      print_usage(stdout)
      1
    rescue CommandFailed
      2
    end

    def self.print_usage(stdout)
      stdout.puts <<-HELP
      Usage: roadshow <command> [<options>]

      Commands:
         help        Get help about a command
         init        Generate a `scenarios.yml` file
         generate    Generate Docker configuration
         run         Run scenarios with Docker
         cleanup     Clean up Docker images
      HELP
    end
  end
end

Add missing commands to usage

Add missing commands to usage


require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = HTTP::WebSocket.new(URI.parse(url))
      @websocket.not_nil!.on_message(->on_message(String))
    end

    private def on_message(message : String)
    end
  end
end

require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = HTTP::WebSocket.new(URI.parse(url))
      @websocket.not_nil!.on_message(&->on_message(String)) # TODO: better error handling
    end

    private def on_message(message : String)
    end
  end
end

Fix the typing for on_message

Fix the typing for on_message


class Exception
  def self.new(message) : NoReturn
    panic
  end
end

class TypeCastError < Exception
  def self.new(message) : NoReturn
    super message
  end
end

class Exception
  def self.new(message, __file__ = __FILE__, __line__ = __LINE__) : NoReturn
    panic message, __file__, __line__
  end
end

class TypeCastError < Exception
  def self.new(message, __file__ = __FILE__, __line__ = __LINE__) : NoReturn
    super message, __file__, __line__
  end
end

Add support for exception messages

Add support for exception messages

Signed-off-by: Splitty <e8820feff88402cc56882fceaf96271e952e1a96@gmail.com>


gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

at = 0
gc = 0

a = 65_u8
t = 84_u8
g = 71_u8
c = 67_u8

gcfile.each_line() do |line|
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |c|
    case c
    when a, t
      at += 1
      next
    when g, c
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)

puts "GC fraction: "
puts gcfrac

gcfile = File.new("Homo_sapiens.GRCh37.67.dna_rm.chromosome.Y.fa")

at = 0
gc = 0

a = 65_u8
t = 84_u8
g = 71_u8
c = 67_u8

gcfile.each_line() do |line|
  if line.starts_with?('>')
    next
  end
  line.each_byte() do |chr|
    case chr
    when a, t
      at += 1
      next
    when g, c
      gc += 1
      next
    end
  end
end

gcfrac = gc / (gc + at)

puts "GC fraction: "
puts gcfrac

Fix bug in Crystal code

Fix bug in Crystal code


require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      else
        puts "Unsupported message: #{message}"
      end
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end
  end
end

require "http/web_socket"
require "json"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = URI.parse(gateway.url)
      @websocket = websocket = HTTP::WebSocket.new(
        host: url.host.not_nil!,
        path: "#{url.path}/?encoding=json&v=6",
        port: 443,
        tls: true
      )

      websocket.on_message(&->on_message(String))
      websocket.on_close(&->on_close(String))
      websocket.run
    end

    private def on_close(message : String)
      # TODO: make more sophisticated
      puts "Closed with: " + message
    end

    OP_HELLO = 10

    private def on_message(message : String)
      json = JSON.parse(message)

      case json["op"]
      when OP_HELLO
        handle_hello(json["d"]["heartbeat_interval"])
      else
        puts "Unsupported message: #{message}"
      end

      nil
    end

    private def handle_hello(heartbeat_interval)
      spawn do
        loop do
          puts "Sending heartbeat"
          @websocket.not_nil!.send({op: 1, d: 0}.to_json)
          sleep heartbeat_interval.as_i.milliseconds
        end
      end
    end
  end
end

Return nil from on_message to satisfy the compiler


module Amber::Router::Session
  # All Session Stores should implement the following API
  abstract class AbstractStore
    abstract def id
    abstract def destroy
    abstract def update(other_hash : Hash(String | Symbol, String))
    abstract def set_session
    abstract def current_session
  end
end

module Amber::Router::Session
  # All Session Stores should implement the following API
  abstract class AbstractStore
    abstract def id
    abstract def destroy
    abstract def update(hash : Hash(String | Symbol, String))
    abstract def set_session
    abstract def current_session
  end
end

Fix naming warning raised by crystal compiler about positional names being mismatched


def majority_element(elements)
  counts = elements.each_with_object(Hash(String, Int32).new(0)) do |element, collector|
    collector[element] += 1
    collector
  end

  result = counts.map do |element, count|
    count > elements.count / 2 ? element : nil
  end.compact

  if result.count == 0
    return nil
  elsif result.count == 1
    return result.first
  end

  raise "Multiple elements found!"
end

elements = File.read_lines(ARGV[0]).first.strip.split(",")
puts majority_element(elements)

def majority_element(elements)
  counts = elements.each_with_object(Hash(String, Int32).new(0)) do |element, collector|
    collector[element] += 1
    collector
  end

  result = counts.map do |element, count|
    count > elements.size / 2 ? element : nil
  end.compact

  if result.size == 0
    return nil
  elsif result.size == 1
    return result.first
  end

  raise "Multiple elements found!"
end

elements = File.read_lines(ARGV[0]).first.strip.split(",")
puts majority_element(elements)

Upgrade crystal (needed it to compare output against Clojure code).


require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8 | Nil,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      channel_id: {type: UInt64, converter: SnowflakeConverter},
      author: User,
      timestamp: {type: Time, converter: Time::Format::ISO_8601_DATE},
      tts: Bool,
      mention_everyone: Bool,
      mentions: Array(User)
    )
  end
end

require "json"
require "time/format"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8 | Nil,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      channel_id: {type: UInt64, converter: SnowflakeConverter},
      author: User,
      timestamp: {type: Time, converter: Time::Format::ISO_8601_DATE},
      tts: Bool,
      mention_everyone: Bool,
      mentions: Array(User)
    )
  end

  struct Overwrite
    JSON.mapping(
      id: {type: UInt64, converter: SnowflakeConverter},
      type: String,
      allow: UInt64,
      deny: UInt64
    )
  end
end

Add a mapping for permission overwrites

Add a mapping for permission overwrites


module Raven
  class Client::State
    enum Status
      ONLINE
      ERROR
    end

    @status : Status
    @retry_number : Int32
    @last_check : Time?
    @retry_after : Time::Span?

    def initialize
      @status = Status::ONLINE
      @retry_number = 0
    end

    def should_try?
      return true if @status.online?

      interval = @retry_after || {@retry_number, 6}.min.seconds
      if timestamp = @last_check
        return true if (Time.now - timestamp) >= interval
      else
        return true
      end
      false
    end

    def failure(retry_after = nil) : Void
      @status = Status::ERROR
      @retry_number += 1
      @last_check = Time.now
      @retry_after = retry_after
    end

    def success : Void
      reset
    end

    def reset : Void
      @status = Status::ONLINE
      @retry_number = 0
      @last_check = nil
      @retry_after = nil
    end

    def failed?
      @status.error?
    end
  end
end

module Raven
  class Client::State
    enum Status
      ONLINE
      ERROR
    end

    @status : Status
    @retry_number : Int32
    @last_check : Time?
    @retry_after : Time::Span?

    def initialize
      @status = Status::ONLINE
      @retry_number = 0
    end

    def should_try?
      return true if @status.online?

      interval = @retry_after || ({@retry_number, 6}.min ** 2).seconds
      if timestamp = @last_check
        return true if (Time.now - timestamp) >= interval
      else
        return true
      end
      false
    end

    def failure(retry_after = nil) : Void
      @status = Status::ERROR
      @retry_number += 1
      @last_check = Time.now
      @retry_after = retry_after
    end

    def success : Void
      reset
    end

    def reset : Void
      @status = Status::ONLINE
      @retry_number = 0
      @last_check = nil
      @retry_after = nil
    end

    def failed?
      @status.error?
    end
  end
end

Fix Client::State exponential backoff logic


module Mnd
  PLATFORMS = {
    "prime" => [
      Repo.new("mynewsdesk", color: :cyan),
      Repo.new("social-media-monitor", color: :green),
      Repo.new("mnd-audience", color: :light_cyan, alias: "audience"),
      Repo.new("mnd-langdetect", color: :light_yellow),
      Repo.new("audience-api", color: :yellow),
      Repo.new("mnd-ui", color: :yellow, alias: "ui"),
      Repo.new("mnd-publish-frontend", color: :yellow, alias: "publish-frontend"),
      Repo.new("mnd-events-api", color: :yellow, alias: "events-api"),
      Repo.new("mnd-track-backend", color: :light_red, alias: "track-backend"),
      Repo.new("mnd-reader-frontend", color: :pink, alias: "reader-frontend"),
    ],

    "poptype" => {
      Repo.new("mndx-analyze", color: :cyan, alias: "analyze"),
      Repo.new("mndx-dashboard", color: :light_blue, alias: "dashboard"),
      Repo.new("mndx-mailer", color: :green, alias: "mailer"),
      Repo.new("mndx-web", color: :red, alias: "web"),
      Repo.new("mndx-amqp", color: :light_red, alias: "amqp"),
      Repo.new("mndx-mailer_client", color: :light_green, alias: "mailer_client"),
    },
  }
end

module Mnd
  PLATFORMS = {
    "prime" => [
      Repo.new("audience-api", color: :yellow),
      Repo.new("mnd-audience", color: :light_cyan, alias: "audience"),
      Repo.new("mnd-events-api", color: :yellow, alias: "events-api"),
      Repo.new("mnd-langdetect", color: :light_yellow, alias: "langdetect"),
      Repo.new("mnd-publish-frontend", color: :yellow, alias: "publish-frontend"),
      Repo.new("mnd-reader-frontend", color: :pink, alias: "reader-frontend"),
      Repo.new("mnd-track-backend", color: :light_red, alias: "track-backend"),
      Repo.new("mnd-ui", color: :yellow, alias: "ui"),
      Repo.new("mynewsdesk", color: :cyan),
      Repo.new("social-media-monitor", color: :green, alias: "smm"),
    ],

    "poptype" => {
      Repo.new("mndx-analyze", color: :cyan, alias: "analyze"),
      Repo.new("mndx-dashboard", color: :light_blue, alias: "dashboard"),
      Repo.new("mndx-mailer", color: :green, alias: "mailer"),
      Repo.new("mndx-web", color: :red, alias: "web"),
      Repo.new("mndx-amqp", color: :light_red, alias: "amqp"),
      Repo.new("mndx-mailer_client", color: :light_green, alias: "mailer_client"),
    },
  }
end

Sort platform repos and add aliases

Sort platform repos and add aliases


struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end
  def [](offset : Int) : T
    self.value
  end
  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
end

struct Pointer(T)
  def +(other : Int)
    self + other.to_i64
  end
  def [](offset : Int) : T
    (self + offset).value
  end
  def []=(offset : Int, value : T)
    (self + offset).value = value
  end
end

Fix [] operator on Pointer(T)

Fix [] operator on Pointer(T)


private PIT_CH0_DATA = 0x40_u16
private PIT_CH1_DATA = 0x41_u16
private PIT_CH2_DATA = 0x42_u16
private PIT_COMMAND = 0x43_u16
private PIT_FREQ = 1193182_u32

struct PIT
  @@ticks = 0_u64
  @@divisor = 0_u32
  @@frequency = 0_u32

  def self.setup(frequency : Int)
    divisor = PIT_FREQ / frequency
    outb PIT_COMMAND, 0x36_u8
    outb PIT_CH0_DATA, divisor.to_u8
    outb PIT_CH0_DATA, (divisor >> 8).to_u8
    @@divisor = divisor
    @@frequency = frequency.to_u32
  end

  def self.tick
    @@ticks += 1
  end

  def self.ticks
    @@ticks
  end

  def self.sleep(ms : Int)
    end_tick_count = @@ticks + ((@@frequency * ms) / 1000)
    while @@ticks < end_tick_count
    end
  end
end

private PIT_CH0_DATA = 0x40_u16
private PIT_CH1_DATA = 0x41_u16
private PIT_CH2_DATA = 0x42_u16
private PIT_COMMAND = 0x43_u16
private PIT_FREQ = 1193182_u32

module PIT
  extend self

  @@ticks = 0_u64
  @@divisor = 0_u32
  @@frequency = 0_u32

  def setup(frequency : Int)
    divisor = PIT_FREQ / frequency
    outb PIT_COMMAND, 0x36_u8
    outb PIT_CH0_DATA, divisor.to_u8
    outb PIT_CH0_DATA, (divisor >> 8).to_u8
    @@divisor = divisor
    @@frequency = frequency.to_u32
  end

  def tick
    @@ticks += 1
  end

  def ticks
    @@ticks
  end

  def sleep(ms : Int)
    end_tick_count = @@ticks + ((@@frequency * ms) / 1000)
    while @@ticks < end_tick_count
    end
  end
end

Change the PIT struct to a module

Change the PIT struct to a module

Signed-off-by: Splitty <e8820feff88402cc56882fceaf96271e952e1a96@gmail.com>


require "http"
require "../http/*"

module Raven
  module Lucky
    # Exception handler capturing all unhandled `Exception`s.
    # After capturing exception is re-raised.
    #
    # ```
    # server = HTTP::Server.new([
    #   # ...
    #   Lucky::ErrorHandler.new(action: Errors::Show),
    #   Raven::Lucky::ErrorHandler,
    #   # ...
    # ])
    # ```
    class ErrorHandler
      include HTTP::Handler
      include Raven::HTTPHandler

      # See `::HTTP::Request`
      CULPRIT_PATTERN_KEYS = %i(method path)

      def initialize(
        @culprit_pattern = "%{method} %{path}",
        @capture_data_for_methods = %w(POST PUT PATCH),
        @default_logger = "lucky"
      )
      end

      def build_raven_culprit_context(context : HTTP::Server::Context)
        context.request
      end

      def build_raven_http_url(context : HTTP::Server::Context)
        Lucky.build_request_url(context.request)
      end

      def build_raven_http_data(context : HTTP::Server::Context)
        ::Lucky::Params.new(context.request).to_h
      end
    end
  end
end

require "http"
require "../http/*"

module Raven
  module Lucky
    # Exception handler capturing all unhandled `Exception`s.
    # After capturing exception is re-raised.
    #
    # ```
    # server = HTTP::Server.new([
    #   # ...
    #   Lucky::ErrorHandler.new(action: Errors::Show),
    #   Raven::Lucky::ErrorHandler.new,
    #   # ...
    # ])
    # ```
    class ErrorHandler
      include HTTP::Handler
      include Raven::HTTPHandler

      # See `::HTTP::Request`
      CULPRIT_PATTERN_KEYS = %i(method path)

      def initialize(
        @culprit_pattern = "%{method} %{path}",
        @capture_data_for_methods = %w(POST PUT PATCH),
        @default_logger = "lucky"
      )
      end

      def build_raven_culprit_context(context : HTTP::Server::Context)
        context.request
      end

      def build_raven_http_url(context : HTTP::Server::Context)
        Lucky.build_request_url(context.request)
      end

      def build_raven_http_data(context : HTTP::Server::Context)
        ::Lucky::Params.new(context.request).to_h
      end
    end
  end
end

Add missing `.new` call in Lucky::ErrorHandler docs


abstract class MainLayout
  include Lucky::HTMLPage
  include Shared::FieldErrorsComponent
  include Shared::FlashComponent

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User

  abstract def content

  def render
    html_doctype

    html lang: "en" do
      head do
        utf8_charset
        title page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
        csrf_meta_tags
      end

      body do
        render_flash
        content
      end
    end
  end

  def page_title
    "Welcome to Lucky"
  end
end

abstract class MainLayout
  include Lucky::HTMLPage
  include Shared::FieldErrorsComponent
  include Shared::FlashComponent

  # You can put things here that all pages need
  #
  # Example:
  #   needs current_user : User

  abstract def content

  def render
    html_doctype

    html lang: "en" do
      head do
        utf8_charset
        title page_title
        css_link asset("css/app.css")
        js_link asset("js/app.js")
        csrf_meta_tags
        responsive_meta_tag
      end

      body do
        render_flash
        content
      end
    end
  end

  def page_title
    "Welcome to Lucky"
  end
end

Include the responsive_meta_tag by default in the main layout


require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = HTTP::WebSocket.new(URI.parse(url))
    end

    private def on_message(message : String)
    end
  end
end

require "http/web_socket"

require "./rest"

module Discordcr
  class Client
    include REST

    def initialize(@token : String, @client_id : UInt64)
    end

    def run
      url = gateway.url
      url += "?v=6&encoding=json"
      @websocket = HTTP::WebSocket.new(URI.parse(url))
      @websocket.on_message(->on_message(String))
    end

    private def on_message(message : String)
    end
  end
end

Fix the typing of the on_message call

Fix the typing of the on_message call


module Messenger::Bot
  # A wrapper for Facebook's Graph API
  class GraphAPI
    # Build an API instance with which you can send messages.
    # Pass *api_version* (current is 2.6) an *access_token*, which you need to
    # authenticate yourself to the Graph API
    def initialize(@api_version : String, @access_token : String)
    end

    # Deliver a pre-built JSON message *message* through the Graph API
    # Returns the delivered message's recipient id and message id
    # Error handling not really built-in.
    def deliver_message(message : String)
      response = HTTP::Client.post("https://graph.facebook.com/v#{@api_version}/me/messages?access_token=#{@access_token}",
        body: message,
        headers: HTTP::Headers{"Content-Type" => "application/json"})
      begin
        result = GraphResponseMessage.from_json(response.body)
        return result.recipient_id, result.message_id
      rescue ex
        puts ex.message
        return 0, 0
      end
    end

    # Notify the user that we're working on a response, and then deliver the
    # final message once it's ready
    # This is still work in progress, reading up on https://crystal-lang.org/docs/guides/concurrency.html
    def deferred_send(recipient_id : Int64, channel : Channel(GraphAPI))
      spawn do
        message = Builder.new(recipient_id)
                  .build_wait_message
        deliver_message(message)
        channel.send(self)
      end
    end
  end
end

module Messenger::Bot
  # A wrapper for Facebook's Graph API
  class GraphAPI
    # Build an API instance with which you can send messages.
    # Pass *api_version* (current is 2.6) an *access_token*, which you need to
    # authenticate yourself to the Graph API
    def initialize(@api_version : String, @access_token : String)
    end

    # Deliver a pre-built JSON message *message* through the Graph API
    # Returns the delivered message's recipient id and message id
    # Error handling not really built-in.
    def deliver_message(message : String)
      response = HTTP::Client.post("https://graph.facebook.com/v#{@api_version}/me/messages?access_token=#{@access_token}",
        body: message,
        headers: HTTP::Headers{"Content-Type" => "application/json"})
      begin
        result = GraphResponseMessage.from_json(response.body)
        return result.recipient_id, result.message_id
      rescue ex
        puts ex.message
        puts response.body
        return 0, 0
      end
    end

    # Notify the user that we're working on a response, and then deliver the
    # final message once it's ready
    # This is still work in progress, reading up on https://crystal-lang.org/docs/guides/concurrency.html
    def deferred_send(recipient_id : Int64, channel : Channel(GraphAPI))
      spawn do
        message = Builder.new(recipient_id)
                  .build_wait_message
        deliver_message(message)
        channel.send(self)
      end
    end
  end
end

Print Graph API error in rescue block

Print Graph API error in rescue block


require "json"

module Scry
  struct InitializeParams
    JSON.mapping({
      process_id:   {type: Int64 | Int32, key: "processId"},
      root_uri:     {type: String, key: "rootPath"},
      capabilities: JSON::Any,
      trace:        String,
    })

  end
end

require "json"

module Scry
  struct InitializeParams
    JSON.mapping({
      process_id:   {type: Int64 | Int32, key: "processId"},
      root_uri:     {type: String, key: "rootPath"},
      capabilities: JSON::Any,
      trace:        String?,
    })

  end
end

Allow trace to be nilable.

Allow trace to be nilable.

This ensures compatibility with Atom and is more accurate to LSP


lib LibGC
  alias WarnProc = LibC::Char*, Word ->
  fun set_warn_proc = GC_set_warn_proc(WarnProc)
  $warn_proc = GC_current_warn_proc : WarnProc
end

LibGC.set_warn_proc ->(msg, word) {

  # Ignore the message in production builds
  {% unless flag?(:release) %}
    puts String.new(msg)
  {% end %}
}

lib LibGC
  alias WarnProc = LibC::Char*, Word ->
  fun set_warn_proc = GC_set_warn_proc(WarnProc)
  $warn_proc = GC_current_warn_proc : WarnProc
end

LibGC.set_warn_proc ->(msg, word) {
  # Ignore the message
}

Hide GC warnings in all builds

Hide GC warnings in all builds


class LuckyCli::Generators::Webpack
  include LuckyCli::GeneratorHelpers

  def initialize(@project_name : String)
    @project_dir = File.join(@project_name)
    @template_dir = File.join(__DIR__, "templates")
  end

  def self.run(project_name : String)
    puts "Adding webpacker config and asset directories"
    new(project_name).install
  end

  def install
    copy_all_templates from: "assets", to: "assets"
    copy_all_templates from: "webpacker", to: "config"
    copy_template from: "root/.babelrc", to: ""
    copy_template from: "root/.postcssrc.yml", to: ""
    add_yarn_deps
  end

  private def add_yarn_deps
    puts "Installing all JavaScript dependencies"
    run_command(
      "yarn add webpack webpack-merge js-yaml path-complete-extname " \
      "webpack-manifest-plugin babel-loader@7.x coffee-loader coffee-script " \
      "babel-core babel-preset-env babel-polyfill compression-webpack-plugin rails-erb-loader glob " \
      "extract-text-webpack-plugin node-sass file-loader sass-loader css-loader style-loader " \
      "postcss-loader postcss-cssnext postcss-smart-import resolve-url-loader " \
      "babel-plugin-syntax-dynamic-import babel-plugin-transform-class-properties"
    )

    puts "Installing dev server for live reloading"
    run_command "yarn add --dev webpack-dev-server"
  end
end

class LuckyCli::Generators::Webpack
  include LuckyCli::GeneratorHelpers

  def initialize(@project_name : String)
    @project_dir = File.join(@project_name)
    @template_dir = File.join(__DIR__, "templates")
  end

  def self.run(project_name : String)
    puts "Adding webpacker config and asset directories"
    new(project_name).install
  end

  def install
    copy_all_templates from: "assets", to: "assets"
    copy_all_templates from: "webpacker", to: "config"
    copy_template from: "root/.babelrc", to: ""
    copy_template from: "root/.postcssrc.yml", to: ""
    add_yarn_deps
  end

  private def add_yarn_deps
    puts "Installing all JavaScript dependencies"
    run_command(
      "yarn add webpack webpack-merge js-yaml path-complete-extname " \
      "webpack-manifest-plugin babel-loader@7.x coffee-loader coffee-script " \
      "babel-core babel-preset-env babel-polyfill compression-webpack-plugin rails-erb-loader glob " \
      "extract-text-webpack-plugin node-sass file-loader sass-loader css-loader style-loader " \
      "postcss-loader postcss-cssnext postcss-smart-import resolve-url-loader " \
      "babel-plugin-syntax-dynamic-import babel-plugin-transform-class-properties" \
      "turoblinks rails-ujs"
    )

    puts "Installing dev server for live reloading"
    run_command "yarn add --dev webpack-dev-server"
  end
end

Install turbolinks and rails-ujs by default


struct Int
    def <<(count) unsafe_shl count end
    def >>(count) unsafe_shr count end
    def ===(other : Int) self == other end
    def /(other : Int)
        if other == 0
            self
        end
        unsafe_div other
    end
    def times
        i = 0
        while i < self
            yield i
            i += 1
        end
    end
end

struct Int
    def <<(count) unsafe_shl count end
    def >>(count) unsafe_shr count end
    def ===(other : Int) self == other end
    def /(other : Int)
        if other == 0
            self
        end
        unsafe_div other
    end
    def %(other : Int)
        if other  == 0
            self
        end
        unsafe_mod other
    end
    def times
        i = 0
        while i < self
            yield i
            i += 1
        end
    end
end

Add % override to Int

Add % override to Int


require "kemal"
require "kemal-session"

module Vulnsearch
  class HomeController
    get "/" do |env|
      query = env.params.query.fetch("q", "")
      render_default "home/index"
    end

    get "/search" do |env|
      query = env.params.query.fetch("q", "")
      like_query = "%#{query}%"
      cves = Cve.from_rs(VULNDB.query("SELECT * FROM cves WHERE id LIKE ? OR summary LIKE ? ORDER BY published DESC", like_query, like_query))

      render_default "home/search"
    end
  end
end

require "kemal"
require "kemal-session"

module Vulnsearch
  class HomeController
    get "/" do |env|
      query = env.params.query.fetch("q", "")
      render_default "home/index"
    end

    get "/search" do |env|
      query = env.params.query.fetch("q", "")
      like_query = "%#{query}%"
      cves = Cve.from_rs(
        VULNDB.query(
          "SELECT * FROM cves " \
          "WHERE id LIKE ? " \
          "OR summary LIKE ? " \
          "ORDER BY id DESC",
          like_query,
          like_query
        )
      )

      render_default "home/search"
    end
  end
end

Order default by id for now

Order default by id for now


module Mocks
  class HaveReceivedExpectation
    def initialize(@receive)
    end

    def match(@target)
      method.received?(oid, @receive.args)
    end

    def failure_message
      "expected: #{expected}\n     got: #{got}"
    end

    def negative_failure_message
      "expected: receive != #{expected}\n     got: #{got}"
    end

    private def method
      Registry
        .for(target_class_name(@target))
        .fetch_method(@receive.method_name)
    end

    private def self_method?
      @receive.method_name.starts_with?("self.")
    end

    private def target_class_name(target)
      return target.name if self_method? && target.is_a?(Class)
      target.class.name
    end

    private def oid
      Registry::ObjectId.build(@target)
    end

    private def got
      if args = last_args
        return "#{@receive.method_name}#{args.inspect}"
      end

      "nil"
    end

    def expected
      "#{@receive.method_name}#{@receive.args.inspect}"
    end

    private def last_args
      method.last_received_args(oid)
    end
  end
end

module Mocks
  class HaveReceivedExpectation
    def initialize(@receive)
    end

    def match(@target)
      method.received?(oid, @receive.args)
    end

    def failure_message
      "expected: #{expected}\n     got: #{got}"
    end

    def failure_message(_ignored)
      failure_message
    end

    def negative_failure_message
      "expected: receive != #{expected}\n     got: #{got}"
    end

    def negative_failure_message(_ignored)
      negative_failure_message
    end

    private def method
      Registry
        .for(target_class_name(@target))
        .fetch_method(@receive.method_name)
    end

    private def self_method?
      @receive.method_name.starts_with?("self.")
    end

    private def target_class_name(target)
      return target.name if self_method? && target.is_a?(Class)
      target.class.name
    end

    private def oid
      Registry::ObjectId.build(@target)
    end

    private def got
      if args = last_args
        return "#{@receive.method_name}#{args.inspect}"
      end

      "nil"
    end

    def expected
      "#{@receive.method_name}#{@receive.args.inspect}"
    end

    private def last_args
      method.last_received_args(oid)
    end
  end
end

Upgrade to a newer version of matcher protocol


def accepts_int(int: Int)
  puts int
end

accepts_int("Not a String.")

def accepts_int(int : Int)
  puts int
end

accepts_int("Not a String.")

Fix method type spec so it returns the correct error.


require "http/client"
require "openssl/ssl/context"

require "./mappings/*"
require "./version"

module Discord
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new
    USER_AGENT  = "DiscordBot (https://github.com/meew0/discordcr, #{Discord::VERSION})"
    API_BASE = "https://discordapp.com/api/v6"

    def request(endpoint_key : Symbol, method : String, path : String, headers : HTTP::Headers, body : String?)
      headers["Authorization"] = @token
      headers["User-Agent"] = USER_AGENT

      HTTP::Client.exec(method: method, url: API_BASE + path, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    def get_gateway
      response = request(
        :get_gateway,
        "GET",
        "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body)
    end

    def create_message(channel_id, content)
      response = request(
        :create_message,
        "POST",
        "/channels/#{channel_id}/messages",
        HTTP::Headers{"Content-Type" => "application/json"},
        {content: content}.to_json
      )

      Message.from_json(response.body)
    end
  end
end

require "http/client"
require "openssl/ssl/context"

require "./mappings/*"
require "./version"

module Discord
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new
    USER_AGENT  = "DiscordBot (https://github.com/meew0/discordcr, #{Discord::VERSION})"
    API_BASE = "https://discordapp.com/api/v6"

    def request(endpoint_key : Symbol, method : String, path : String, headers : HTTP::Headers, body : String?)
      headers["Authorization"] = @token
      headers["User-Agent"] = USER_AGENT

      HTTP::Client.exec(method: method, url: API_BASE + path, headers: headers, body: body, tls: SSL_CONTEXT)
    end

    def get_gateway
      response = request(
        :get_gateway,
        "GET",
        "/gateway",
        HTTP::Headers.new,
        nil
      )

      GatewayResponse.from_json(response.body)
    end

    def get_channel(channel_id)
      response = request(
        :get_channel,
        "GET",
        "/channels/#{channel_id}",
        HTTP::Headers.new,
        nil
      )

      Channel.from_json(response.body)
    end

    def create_message(channel_id, content)
      response = request(
        :create_message,
        "POST",
        "/channels/#{channel_id}/messages",
        HTTP::Headers{"Content-Type" => "application/json"},
        {content: content}.to_json
      )

      Message.from_json(response.body)
    end
  end
end

Add a method for the get channel endpoint


require "./converters"
require "./user"
require "./channel"

module Discord
  module Gateway
    struct ReadyPayload
      JSON.mapping(
        v: UInt8,
        user: User,
        private_channels: Array(PrivateChannel),
        guilds: Array(UnavailableGuild),
        session_id: String
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end

    struct GuildDeletePayload
      JSON.mapping(
        id: {type: UInt64, converter: SnowflakeConverter},
        unavailable: {type: Bool, nilable: true}
      )
    end
  end
end

require "./converters"
require "./user"
require "./channel"

module Discord
  module Gateway
    struct ReadyPayload
      JSON.mapping(
        v: UInt8,
        user: User,
        private_channels: Array(PrivateChannel),
        guilds: Array(UnavailableGuild),
        session_id: String
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end

    struct GuildDeletePayload
      JSON.mapping(
        id: {type: UInt64, converter: SnowflakeConverter},
        unavailable: {type: Bool, nilable: true}
      )
    end

    struct GuildBanPayload
      JSON.mapping(
        username: String,
        id: {type: UInt64, converter: SnowflakeConverter},
        discriminator: String,
        avatar: String,
        bot: {type: Bool, nilable: true},
        guild_id: {type: UInt64, converter: SnowflakeConverter}
      )
    end
  end
end

Add a mapping for the payload used by dispatches dealing with bans


class UserBox < Avram::Box
  def initialize
    email "test@example.com"
    encrypted_password Authentic.generate_encrypted_password("password")
  end
end

class UserBox < Avram::Box
  def initialize
    email "#{sequence("test-email")}@example.com"
    encrypted_password Authentic.generate_encrypted_password("password")
  end
end

Use unique email address in UserBox

Use unique email address in UserBox


require "./stream"
require "./row_value"
require "../../storage/*"

module ReQL
  struct GetAllStream < Stream
    class InternalData
      property channels = [] of Channel(RowValue?)
    end

    def reql_type
      "STREAM"
    end

    def initialize(@table : Storage::AbstractTable, @keys : Array(Datum) | Set(Datum), @index : String)
      @internal = InternalData.new
    end

    def start_reading
      @keys.each do |key|
        channel = Channel(RowValue?).new(16)
        @internal.channels << channel
        spawn do
          begin
            if @index == @table.primary_key
              channel.try { |ch| @table.get(key).try { |row| ch.send RowValue.new(@table, row) } }
            else
              @table.index_scan(@index, key, key) do |row|
                channel.try &.send RowValue.new(@table, row)
              end
            end
            channel.try &.send nil
          rescue Channel::ClosedError
          end
        end
      end
    end

    def next_val
      return nil if @internal.channels.empty?
      idx, val = Channel.select(@internal.channels.map &.receive_select_action?)
      unless val
        @internal.channels[idx].try &.close
        @internal.channels.reject! &.closed?
        return next_val
      end
      val
    end

    def finish_reading
      @internal.channels.each &.close
      @internal.channels.clear
    end
  end
end

require "./stream"
require "./row_value"
require "../../storage/*"

module ReQL
  struct GetAllStream < Stream
    class InternalData
      property channels = [] of Channel(RowValue?)
    end

    def reql_type
      "STREAM"
    end

    def initialize(@table : Storage::AbstractTable, @keys : Array(Datum) | Set(Datum), @index : String)
      @internal = InternalData.new
    end

    def start_reading
      @keys.each do |key|
        channel = Channel(RowValue?).new(16)
        @internal.channels << channel
        spawn do
          begin
            if @index == @table.primary_key
              @table.get(key).try { |row| channel.send RowValue.new(@table, row) }
            else
              @table.index_scan(@index, key, key) do |row|
                channel.send RowValue.new(@table, row)
              end
            end
            channel.send nil
          rescue Channel::ClosedError
          end
        end
      end
    end

    def next_val
      return nil if @internal.channels.empty?
      idx, val = Channel.select(@internal.channels.map &.receive_select_action?)
      unless val
        @internal.channels[idx].try &.close
        @internal.channels.reject! &.closed?
        return next_val
      end
      val
    end

    def finish_reading
      @internal.channels.each &.close
      @internal.channels.clear
    end
  end
end

Simplify GetAllStream: channel is never null


require "../src/microtest"
require "./helpers"

include Microtest::DSL
include Helpers

COLOR_REGEX = %r{\e\[\d\d?(;\d)?m}

def uncolor(str)
  str.gsub(COLOR_REGEX, "")
end

macro microtest_test(&block)
  {%
    c = <<-CRYSTAL
      require "./src/microtest"

      include Microtest::DSL

      #{block.body.id}

      Microtest.run!([
        Microtest::JsonSummaryReporter.new
      ] of Microtest::Reporter)
    CRYSTAL
  %}

  output = IO::Memory.new

  s = Process.run("crystal", ["eval", {{c}}, "--error-trace"], output: output, error: STDERR)

  begin
    MicrotestJsonResult.new(s, JSON.parse(output.to_s))
  rescue e
    raise "Error parsing JSON: #{output.to_s.inspect}"
  end
end

macro reporter_test(reporters, &block)
  {%
    c = <<-CRYSTAL
      require "./src/microtest"

      include Microtest::DSL

      #{block.body.id}

      Microtest.run!(#{reporters} of Microtest::Reporter)
    CRYSTAL
  %}

  output = IO::Memory.new
  err = IO::Memory.new

  s = Process.run("crystal", ["eval", {{c}}, "--error-trace"], output: output, error: err)

  MicrotestStdoutResult.new(s, output.to_s, err.to_s)
end

def generate_assets?
  ENV.has_key?("ASSETS")
end

Microtest.run!(:descriptions)

Use progress reporter for tests

Use progress reporter for tests


Amber::Server.instance.config do |app|
  pipeline :web do
    # Plug is the method to use connect a pipe (middleware)
    # A plug accepts an instance of HTTP::Handler
    plug Amber::Pipe::Logger.new unless app.env == "test"
    plug Amber::Pipe::Flash.new
    plug Amber::Pipe::Session.new
    plug Amber::Pipe::CSRF.new
  end

  # All static content will run these transformations
  pipeline :static do
    plug HTTP::StaticFileHandler.new("./public")
    plug HTTP::CompressHandler.new
  end

  routes :static do
    # Each route is defined as follow
    # verb resource : String, controller : Symbol, action : Symbol
    get "/*", StaticController, :index
    get "/about", StaticController, :about
  end

  routes :web do
    resources "/announcements", AnnouncementController
    get "/=:hashid", AnnouncementController, :expand
    get "/rss", RSSController, :show
    get "/sessions/new", SessionsController, :new
    delete "/sessions", SessionsController, :destroy
    get "/github/auth", SessionsController, :create
    get "/me", UserController, :me
    get "/users/:login", UserController, :show
    get "/", AnnouncementController, :index
  end
end

Amber::Server.instance.config do |app|
  pipeline :web do
    # Plug is the method to use connect a pipe (middleware)
    # A plug accepts an instance of HTTP::Handler
    plug Amber::Pipe::Logger.new unless app.env == "test"
    plug Amber::Pipe::Flash.new
    plug Amber::Pipe::Session.new
    plug Amber::Pipe::CSRF.new
  end

  # All static content will run these transformations
  pipeline :static do
    plug Amber::Pipe::Static.new("./public")
    plug HTTP::CompressHandler.new
  end

  routes :static do
    # Each route is defined as follow
    # verb resource : String, controller : Symbol, action : Symbol
    get "/*", StaticController, :index
    get "/about", StaticController, :about
  end

  routes :web do
    resources "/announcements", AnnouncementController
    get "/=:hashid", AnnouncementController, :expand
    get "/rss", RSSController, :show
    get "/sessions/new", SessionsController, :new
    delete "/sessions", SessionsController, :destroy
    get "/github/auth", SessionsController, :create
    get "/me", UserController, :me
    get "/users/:login", UserController, :show
    get "/", AnnouncementController, :index
  end
end

Use Amber::Pipe::Static handler to set http response cache headers

Enables caching of static files in `public/` dir which
will improve page load time and prevent delays in applying css/js to the
dom.

https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers


require "./spec_helper"

describe Crustache do
  describe ".parse" do
    it "shold parse a string" do
      Crustache.parse("Hello, {{Mustache}} World").should be_truthy
    end

    it "should parse a IO" do
      Crustache.parse(StringIO.new "Hello, {{Mustache}} World").should be_truthy
    end

    it "raise a parse error" do
      expect_raises(Crustache::ParseError) do
        Crustache.parse("Hello, {{Mustache? World")
      end
    end
  end

  describe ".parseFile" do
    it "should parse a file" do
      Crustache.parse("#{__DIR__}/view/template.mustache").should be_truthy
    end
  end
end

require "./mustache_spec"
require "./view_loader_spec"

require "./spec_helper"

describe Crustache do
  describe "#parse" do
    it "shold parse a string" do
      Crustache.parse("Hello, {{Mustache}} World").should be_truthy
    end

    it "should parse a IO" do
      Crustache.parse(StringIO.new "Hello, {{Mustache}} World").should be_truthy
    end

    it "raise a parse error" do
      expect_raises(Crustache::ParseError) do
        Crustache.parse("Hello, {{Mustache? World")
      end
    end
  end

  describe "#parseFile" do
    it "should parse a file" do
      Crustache.parse("#{__DIR__}/view/template.mustache").should be_truthy
    end
  end
end

require "./mustache_spec"
require "./view_loader_spec"
require "./engine_spec"

Add Engine's spec to crustache specs

Add Engine's spec to crustache specs


module Jennifer
  module Migration
    module TableBuilder
      abstract class Base
        # Base allowed types for migration DSL option values
        alias AllowedTypes = String | Int32 | Bool | Float32 | Float64 | JSON::Any | Nil
        # Allowed types for migration DSL + Symbol
        alias EAllowedTypes = AllowedTypes | Symbol
        # Allowed types for migration DSL including array
        alias AAllowedTypes = EAllowedTypes | Array(EAllowedTypes)
        # Hash type for options argument
        alias DB_OPTIONS = Hash(Symbol, EAllowedTypes | Array(EAllowedTypes))

        delegate schema_processor, table_exists?, index_exists?, column_exists?, to: adapter

        getter adapter : Adapter::Base, name : String

        def initialize(@adapter, name : String | Symbol)
          @name = name.to_s
          @commands = [] of Base
        end

        abstract def process
        abstract def explain

        def process_commands
          @commands.each(&.process)
        end

        private def build_column_options(type : Symbol?, options : Hash)
          if type.nil? && !options.has_key?(:sql_type)
            raise ArgumentError.new("Both type and sql_type can't be blank")
          end

          Ifrit.sym_hash_cast(options, AAllowedTypes).merge({ :type => type } of Symbol => AAllowedTypes)
        end
      end
    end
  end
end

module Jennifer
  module Migration
    module TableBuilder
      abstract class Base
        # Base allowed types for migration DSL option values
        alias AllowedTypes = String | Int32 | Int64 | Bool | Float32 | Float64 | JSON::Any | Nil
        # Allowed types for migration DSL + Symbol
        alias EAllowedTypes = AllowedTypes | Symbol
        # Allowed types for migration DSL including array
        alias AAllowedTypes = EAllowedTypes | Array(EAllowedTypes)
        # Hash type for options argument
        alias DB_OPTIONS = Hash(Symbol, EAllowedTypes | Array(EAllowedTypes))

        delegate schema_processor, table_exists?, index_exists?, column_exists?, to: adapter

        getter adapter : Adapter::Base, name : String

        def initialize(@adapter, name : String | Symbol)
          @name = name.to_s
          @commands = [] of Base
        end

        abstract def process
        abstract def explain

        def process_commands
          @commands.each(&.process)
        end

        private def build_column_options(type : Symbol?, options : Hash)
          if type.nil? && !options.has_key?(:sql_type)
            raise ArgumentError.new("Both type and sql_type can't be blank")
          end

          Ifrit.sym_hash_cast(options, AAllowedTypes).merge({ :type => type } of Symbol => AAllowedTypes)
        end
      end
    end
  end
end

Add Int64 to Jennifer::Migration::TableBuilder::Base::AllowedTypes alias


require "spec"
require "mocks"
require "../src/active_record"
require "../src/null_adapter"
require "../src/criteria_helper"
require "./fake_adapter"

# Use this after next release of Crystal
#Spec.before_each do
#  ActiveRecord::NullAdapter.reset
#end
#
#Spec.after_each do
#  ActiveRecord::NullAdapter.reset
#end

# Now use this:
def it(description, file = __FILE__, line = __LINE__, &block)
  ActiveRecord::NullAdapter.reset
  FakeAdapter._reset
  previous_def(description, file, line, &block)
  ActiveRecord::NullAdapter.reset
  FakeAdapter._reset
end

class SameQueryExpectation(T)
  def initialize(@expected : T)
  end

  def match(@actual)
    ActiveRecord::QueryObject.same_query?(@expected, @actual)
  end

  def failure_message
    "expected: #{@expected.inspect}\n     got: #{@actual.inspect}"
  end

  def negative_failure_message
    "expected: value != #{@expected.inspect}\n     got: #{@actual.inspect}"
  end
end

def be_same_query(expected)
  SameQueryExpectation.new(expected)
end

include ActiveRecord::CriteriaHelper

def example(description, file = __FILE__, line = __LINE__, &block)
  it(description, file, line, &block)
end

require "spec"
require "mocks"
require "../src/active_record"
require "../src/null_adapter"
require "../src/criteria_helper"
require "./fake_adapter"

def _specs_reset
  ActiveRecord::NullAdapter.reset
  FakeAdapter._reset
end

Spec.before_each do
  _specs_reset
end

Spec.after_each do
  _specs_reset
end

class SameQueryExpectation(T)
  def initialize(@expected : T)
  end

  def match(@actual)
    ActiveRecord::QueryObject.same_query?(@expected, @actual)
  end

  def failure_message
    "expected: #{@expected.inspect}\n     got: #{@actual.inspect}"
  end

  def negative_failure_message
    "expected: value != #{@expected.inspect}\n     got: #{@actual.inspect}"
  end
end

def be_same_query(expected)
  SameQueryExpectation.new(expected)
end

include ActiveRecord::CriteriaHelper

def example(description, file = __FILE__, line = __LINE__, &block)
  it(description, file, line, &block)
end

Update spec helper to use global before and after hooks


require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    struct MessageCreatePayload
      JSON.mapping(
        type: UInt8,
        content: String,
        id: String,
        author: User
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: String,
      discriminator: String,
      avatar: String
    )
  end
end

require "json"

module Discordcr
  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct MessageCreatePayload
      JSON.mapping(
        type: UInt8,
        content: String,
        id: String,
        author: User
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: String,
      discriminator: String,
      avatar: String
    )
  end
end

Define a very basic mapping for the READY payload


require "./patches/**"
require "./caoutchouc/**"

module Caoutchouc
  class CLI
    include Shell

    def initialize(args)
      debug "Observing cluster at: #{args.inspect}"
      puts STDIN.tty?
      if args.size == 0
        location = "http://localhost:9200"
      else
        location = args.first
      end
      Caoutchouc::Elasticsearch.set_location(location)
      main_loop()
    end
  end
end

Caoutchouc::CLI.new(ARGV)

require "./patches/**"
require "./caoutchouc/**"

module Caoutchouc
  class CLI
    include Shell

    def initialize(args)
      debug "Observing cluster at: #{args.inspect}"
      if args.size == 0
        location = "http://localhost:9200"
      else
        location = args.first
      end
      Caoutchouc::Elasticsearch.set_location(location)
      main_loop()
    end
  end
end

Caoutchouc::CLI.new(ARGV)

Remove debug message mistakenly committed in 14f84ae52


require "./../spec_helper"

describe M3U8::Playlist do
  context "when creating a playlist" do
    let(playlist) { M3U8::Playlist.new }

    it "inserts the header" do
      expect(playlist.to_s).to match(/#EXTM3U\n/)
    end

    it "accepts new segments" do
      playlist.add_segment("S01E01-1080-0001.ts", 9.003)

      output = "#EXTINF:9.003,\n" \
        "S01E01-1080-0001.ts\n"

      expect(playlist.to_s).to match(/#{output}/)
    end
  end
end

require "./../spec_helper"

describe M3U8::Playlist do
  context "when creating a playlist" do
    let(playlist) { M3U8::Playlist.new }

    it "inserts the header" do
      expect(playlist.to_s).to match(/#EXTM3U\n/)
    end

    it "inserts an empty line between the header and rest" do
      expect(playlist.to_s.lines[1]).to eq("\n")
    end

    it "accepts new segments" do
      playlist.add_segment("S01E01-1080-0001.ts", 9.003)

      output = "#EXTINF:9.003,\n" \
        "S01E01-1080-0001.ts\n"

      expect(playlist.to_s).to match(/#{output}/)
    end
  end
end

test: Validate presence of line between the header and the rest (96948a8)


require "http/client"
require "openssl/ssl/context"

require "./mappings"
require "./version"

module Discord
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new
    USER_AGENT  = "DiscordBot (https://github.com/meew0/discordcr, #{Discord::VERSION})"

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers, body : String | Nil)
      headers["Authorization"] = @token
      headers["User-Agent"] = USER_AGENT

      spawn { HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT) }
    end

    API_BASE = "https://discordapp.com/api/v6"

    GET  = "GET"
    POST = "POST"

    def gateway
      response = request(
        :gateway,
        GET,
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      # TODO: Investigate failed nil assertion with body_io
      GatewayResponse.from_json(response.body)
    end

    def send_message(channel_id, content)
      response = request(
        :send_message,
        POST,
        API_BASE + "/channels/#{channel_id}/messages",
        HTTP::Headers{"Content-Type" => "application/json"},
        {content: content}.to_json
      )
    end
  end
end

require "http/client"
require "openssl/ssl/context"

require "./mappings"
require "./version"

module Discord
  module REST
    SSL_CONTEXT = OpenSSL::SSL::Context::Client.new
    USER_AGENT  = "DiscordBot (https://github.com/meew0/discordcr, #{Discord::VERSION})"

    def request(endpoint_key : Symbol, method : String, url : String | URI, headers : HTTP::Headers, body : String | Nil)
      headers["Authorization"] = @token
      headers["User-Agent"] = USER_AGENT

      spawn { HTTP::Client.exec(method: method, url: url, headers: headers, body: body, tls: SSL_CONTEXT) }
    end

    API_BASE = "https://discordapp.com/api/v6"

    def gateway
      response = request(
        :gateway,
        "GET",
        API_BASE + "/gateway",
        HTTP::Headers.new,
        nil
      )

      # TODO: Investigate failed nil assertion with body_io
      GatewayResponse.from_json(response.body)
    end

    def send_message(channel_id, content)
      response = request(
        :send_message,
        "POST",
        API_BASE + "/channels/#{channel_id}/messages",
        HTTP::Headers{"Content-Type" => "application/json"},
        {content: content}.to_json
      )
    end
  end
end

Use direct string literals instead of constants for HTTP methods


require "json"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct MessageCreatePayload
      JSON.mapping(
        type: UInt8,
        content: String,
        id: {type: UInt64, converter: SnowflakeConverter},
        author: User
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end
end

require "json"

module Discord
  module SnowflakeConverter
    def self.from_json(parser : JSON::PullParser) : UInt64
      str = parser.read_string_or_null
      str.not_nil!.to_u64
    end

    def self.to_json(value : UInt64, io : IO)
      io.puts(value.to_s)
    end
  end

  module REST
    # A response to the Get Gateway REST API call.
    struct GatewayResponse
      JSON.mapping(
        url: String
      )
    end
  end

  module Gateway
    # TODO: Expand this
    struct ReadyPayload
      JSON.mapping(
        v: UInt8
      )
    end

    struct HelloPayload
      JSON.mapping(
        heartbeat_interval: UInt32,
        _trace: Array(String)
      )
    end
  end

  struct User
    JSON.mapping(
      username: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      discriminator: String,
      avatar: String
    )
  end

  struct Message
    JSON.mapping(
      type: UInt8,
      content: String,
      id: {type: UInt64, converter: SnowflakeConverter},
      author: User
    )
  end
end

Move Gateway::MessageCreatePayload to simply Message

The two are identical in format so there's no reason to use have duplication
here.


module Discordcr
  class Client
    def initialize(@token : String, @client_id : UInt64)
    end
  end
end

module Discordcr
  class Client
    def initialize(@token : String, @client_id : UInt64)
    end

    def run
    end
  end
end

Add a run method to Client

Add a run method to Client


require "../spec_helper"

module Franklin
  class ConsoleReportLet
    property subject : ConsoleReport
    property result : String
    property availability_description : AvailabilityDescription
    property search_terms : String
    property item : Item

    def initialize(@search_terms = "crime and punishment")
      @item = Item.random_fixture
      availability = Availability.random_fixture
      collated_results = {item => [availability]}
      io = IO::Memory.new

      @subject = ConsoleReport.new(search_terms, collated_results)
      @result = subject.to_s(io).to_s
      @availability_description = AvailabilityDescription.new(availability, item)
    end
  end

  describe ConsoleReport do
    describe ".to_s" do
      it "includes search_term" do
        let = ConsoleReportLet.new
        let.result.should match(/#{let.search_terms}/)
      end

      it "includes item information" do
        let = ConsoleReportLet.new
        result = let.result
        item = let.item

        result.should match(/^#{item.title}/m)
        result.should match(/^By #{item.author}/m)
        result.should match(/^Format: #{item.format}/m)
      end

      it "includes availability information" do
        let = ConsoleReportLet.new
        result = let.result
        availability_description = let.availability_description

        result.should match(/^Availability:/m)
        result.should match(/^  #{availability_description.to_s}/m)
        result.should match(/^    #{availability_description.url}/m)
      end
    end
  end
end

require "../spec_helper"

module Franklin
  describe ConsoleReport do
    subject { ConsoleReport.new(search_terms, collated_results) }
    let(:search_terms) { "crime and punishment" }
    let(:collated_results) { {item => [availability]} }
    let(:result) { subject.to_s(io).to_s }
    let(:io) { IO::Memory.new }
    let(:item) { Item.random_fixture }
    let(:availability) { Availability.random_fixture }
    let(:availability_description) { AvailabilityDescription.new(availability, item) }

    describe ".to_s" do
      it "includes search_term" do
        expect(result).to match(/#{search_terms}/)
      end

      it "includes item information" do
        expect(result).to match(/^#{item.title}/m)
        expect(result).to match(/^By #{item.author}/m)
        expect(result).to match(/^Format: #{item.format}/m)
      end

      it "includes availability information" do
        expect(result).to match(/^Availability:/m)
        expect(result).to match(/^  #{availability_description.to_s}/m)
        expect(result).to match(/^    #{availability_description.url}/m)
      end
    end
  end
end

Revert "Expriment with a Let class to hold setup"

This reverts commit 4470fd705897344b0b2781de54cae53f844ea386.