zold-io/wts.zold.io

View on GitHub
wts.rb

Summary

Maintainability
C
1 day
Test Coverage
# Copyright (c) 2018-2024 Zerocracy
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

$stdout.sync = true

require 'backtrace'
require 'concurrent'
require 'get_process_mem'
require 'geoplugin'
require 'glogin'
require 'haml'
require 'iri'
require 'json'
require 'pgtk/pool'
require 'rack/ssl'
require 'raven'
require 'sass'
require 'securerandom'
require 'sinatra'
require 'sinatra/cookies'
require 'telebot'
require 'telepost'
require 'tempfile'
require 'total'
require 'uri'
require 'yaml'
require 'zold'
require 'zold/amount'
require 'zold/cached_wallets'
require 'zold/hands'
require 'zold/json_page'
require 'zold/log'
require 'zold/remotes'
require 'zold/sync_wallets'
require_relative 'objects/daemons'
require_relative 'objects/item'
require_relative 'objects/ops'
require_relative 'objects/payouts'
require_relative 'objects/tokens'
require_relative 'objects/user'
require_relative 'objects/wts'
require_relative 'objects/dollars'
require_relative 'objects/rate'
require_relative 'version'

if ENV['RACK_ENV'] != 'test'
  require 'rack/ssl'
  use Rack::SSL
end

# See https://github.com/baldowl/rack_csrf
require 'rack/csrf'
use Rack::Session::Cookie
use Rack::Csrf, raise: true, skip_if: lambda { |request|
  request.env.key?('HTTP_X_ZOLD_WTS')
}

configure do
  Zold::Hands.start
  Haml::Options.defaults[:format] = :xhtml
  config = if ENV['RACK_ENV'] == 'test'
    {
      'kyc' => [],
      'pkey_secret' => 'fake',
      'geoplugin_token' => '?',
      'rewards' => {
        'login' => 'zonuses',
        'keygap' => '?'
      },
      'exchange' => {
        'login' => 'zoldwts',
        'keygap' => '?'
      },
      'zerocrat' => {
        'login' => '0crat',
        'keygap' => '?'
      },
      'paypal' => {
        'id' => '?',
        'secret' => '?'
      },
      'github' => {
        'client_id' => '?',
        'client_secret' => '?',
        'encryption_secret' => ''
      },
      'api_secret' => 'test',
      'sentry' => '',
      'telegram' => {
        'token' => '',
        'chat' => '111'
      }
    }
  else
    YAML.safe_load(File.open(File.join(File.dirname(__FILE__), 'config.yml')))
  end
  if ENV['RACK_ENV'] != 'test'
    Raven.configure do |c|
      c.dsn = config['sentry']
      c.release = WTS::VERSION
    end
  end
  set :bind, '0.0.0.0'
  set :server, :thin
  set :config, config
  set :logging, true
  set :show_exceptions, false
  set :raise_errors, false
  set :dump_errors, false
  set :server_settings, timeout: 25
  set :log, ENV['RACK_ENV'] == 'test' ? Zold::Log::VERBOSE.dup : Zold::Log::REGULAR.dup
  set :log, Zold::Log::NULL if ENV['TEST_QUIET_LOG']
  set :glogin, GLogin::Auth.new(
    config['github']['client_id'],
    config['github']['client_secret'],
    'https://wts.zold.io/github-callback'
  )
  set :wallets, Zold::SyncWallets.new(
    Zold::CachedWallets.new(
      Zold::Wallets.new(
        File.join(settings.root, '.zold-wts/wallets')
      )
    )
  )
  set :remotes, Zold::Remotes.new(
    file: File.join(settings.root, '.zold-wts/remotes'),
    network: ENV['RACK_ENV'] == 'test' ? 'test' : 'zold'
  )
  if File.exist?('target/pgsql-config.yml')
    set :pgsql, Pgtk::Pool.new(
      Pgtk::Wire::Yaml.new(File.join(__dir__, 'target/pgsql-config.yml')),
      log: settings.log
    )
  else
    set :pgsql, Pgtk::Pool.new(
      Pgtk::Wire::Env.new('DATABASE_URL'),
      log: settings.log
    )
  end
  settings.pgsql.start(4)
  set :copies, File.join(settings.root, '.zold-wts/copies')
  set :payouts, WTS::Payouts.new(settings.pgsql, log: settings.log)
  set :daemons, WTS::Daemons.new(settings.pgsql, log: settings.log)
  set :codec, GLogin::Codec.new(config['api_secret'], base64: true)
  set :zache, Zache.new(dirty: true)
  if settings.config['telegram']['token'].empty?
    set :telepost, Telepost::Fake.new
  else
    chat = '@zold_wts'
    set :telepost, Telepost.new(
      settings.config['telegram']['token'],
      chats: [chat]
    )
    settings.daemons.start('telepost') do
      settings.log.info("Starting Telegram chatbot at #{chat}...")
      settings.telepost.run do |cht, _msg|
        settings.telepost.post(
          cht,
          'This bot is not answering here.',
          'All it does is posting news to this channel: [@zold_wts](https://t.me/zold_wts).',
          'Don\'t hesitate to subscribe and stay informed about everything that is going on',
          'in https://wts.zold.io.'
        )
      end
    end
  end
  settings.telepost.spam(
    '👋 [WTS](https://wts.zold.io) server software',
    "[#{WTS::VERSION}](https://github.com/zold-io/wts.zold.io/releases/tag/#{WTS::VERSION})",
    'has been deployed and starts working;',
    "Zold version is [#{Zold::VERSION}](https://rubygems.org/gems/zold/versions/#{Zold::VERSION}),",
    "the protocol is `#{Zold::PROTOCOL}`;",
    "#{format('%.01f', Total::Mem.new.bytes.to_f / (1024 * 1024 * 1024))}Gb memory total;",
    "#{Concurrent.physical_processor_count} CPUs"
  )
end

after do
  headers['Access-Control-Allow-Origin'] = '*'
end

get '/' do
  redirect '/home' if @locals[:guser]
  haml :index, layout: :layout, locals: merged(
    page_title: 'wts',
    rate: WTS::Rate.new(settings.toggles).to_f
  )
end

get '/home' do
  unless user.item.exists?
    flash('/create', 'Time to create your wallet') unless File.exist?(latch(user.login))
    return haml :busy, layout: :layout, locals: merged(
      page_title: title('busy')
    )
  end
  flash('/confirm', 'Time to save your keygap') unless user.confirmed?
  haml :home, layout: :layout, locals: merged(
    page_title: title,
    start: params[:start] ? Time.parse(params[:start]) : nil,
    usd_rate: WTS::Rate.new(settings.toggles).to_f * price
  )
end

get '/key' do
  haml :key, layout: :layout, locals: merged(
    page_title: title('key')
  )
end

get '/id' do
  content_type 'text/plain'
  return Zold::Id::ROOT.to_s if user.fake?
  user.item.id.to_s
end

get '/balance' do
  content_type 'text/plain'
  confirmed_user.wallet(&:balance).to_i.to_s
end

get '/head.json' do
  content_type 'application/json'
  confirmed_user.wallet do |wallet|
    JSON.pretty_generate(
      id: wallet.id.to_s,
      mtime: wallet.mtime.utc.iso8601,
      age: wallet.age.to_s,
      size: wallet.size,
      digest: wallet.digest,
      balance: wallet.balance.to_i,
      txns: wallet.txns.count,
      taxes: Zold::Tax.new(wallet).paid.to_i,
      debt: Zold::Tax.new(wallet).debt.to_i
    )
  end
end

get '/find' do
  content_type 'text/plain'
  confirmed_user.wallet do |wallet|
    wallet.txns.select do |t|
      matches = false
      matches |= params[:id] && Regexp.new(params[:id]).match?(t.id.to_s)
      matches |= params[:date] && Regexp.new(params[:date]).match?(t.date.utc.iso8601)
      matches |= params[:amount] && Regexp.new(params[:amount]).match?(t.amount.to_i.to_s)
      matches |= params[:prefix] && Regexp.new(params[:prefix]).match?(t.prefix)
      matches |= params[:bnf] && Regexp.new(params[:bnf]).match?(t.bnf.to_s)
      matches |= params[:details] && Regexp.new(params[:details]).match?(t.details)
      matches
    end
  end.join("\n")
end

get '/txns.json' do
  content_type 'application/json'
  confirmed_user.wallet do |wallet|
    list = wallet.txns
    list.reverse! if params[:sort] && params[:sort] == 'desc'
    JSON.pretty_generate(
      list.map do |t|
        t.to_json.merge(tid: t.amount.negative? ? "#{wallet.id}:#{t.id}" : "#{t.bnf}:#{t.id}")
      end
    )
  end
end

get '/txn.json' do
  tid = params[:tid]
  raise WTS::UserError, "E193: Parameter 'tid' is mandatory" if tid.nil?
  source, id = tid.split(':')
  raise WTS::UserError, "E199: The wallet ID #{source.inspect} is wrong" unless /^[0-9a-f]{16}$/.match?(source)
  raise WTS::UserError, "E200: The transaction ID #{id.inspect} is wrong" unless /^[0-9]+$/.match?(id)
  id = id.to_i
  content_type 'application/json'
  JSON.pretty_generate(settings.gl.txn(source, id))
end

get '/id_rsa' do
  response.headers['Content-Type'] = 'application/octet-stream'
  response.headers['Content-Disposition'] = 'attachment; filename=id_rsa'
  confirmed_user.item.key(keygap).to_s
end

get '/download' do
  response.headers['Content-Type'] = 'application/octet-stream'
  response.headers['Content-Disposition'] = "attachment; filename=#{confirmed_user.item.id}#{Zold::Wallet::EXT}"
  confirmed_user.wallet do |w|
    File.read(w.path)
  end
end

get '/api' do
  features('api')
  haml :api, layout: :layout, locals: merged(
    page_title: title('api'),
    token: "#{confirmed_user.login}-#{settings.tokens.get(confirmed_user.login)}"
  )
end

get '/api-reset' do
  features('api')
  settings.tokens.reset(confirmed_user.login)
  settings.telepost.spam(
    "API token has been reset by #{title_md}",
    "from #{anon_ip}"
  )
  flash('/api', 'You got a new API token')
end

get '/invoice' do
  haml :invoice, layout: :layout, locals: merged(
    page_title: title('invoice')
  )
end

get '/invoice.json' do
  inv = user.invoice
  prefix, id = inv.split('@')
  content_type 'application/json'
  JSON.pretty_generate(prefix: prefix, invoice: inv, id: id)
end

get '/sql' do
  raise WTS::UserError, 'E129: You are not allowed to see this' unless vip?
  query = params[:query] || 'SELECT * FROM txn LIMIT 16'
  haml :sql, layout: :layout, locals: merged(
    page_title: title('SQL'),
    query: query,
    result: settings.pgsql.exec(query)
  )
end

get '/payouts' do
  haml :payouts, layout: :layout, locals: merged(
    page_title: title('payouts'),
    payouts: settings.payouts,
    system_limits: settings.toggles.get('system-limits'),
    limits: settings.toggles.get('limits'),
    system_consumed: settings.payouts.system_consumed,
    consumed: settings.payouts.consumed(confirmed_user.login)
  )
end

get '/buy-sell' do
  features('buy-sell')
  haml :buy_sell, layout: :layout, locals: merged(
    page_title: title('buy/sell')
  )
end

get '/log' do
  content_type 'text/plain', charset: 'utf-8'
  msg = [
    'If you see any errors here, which you don\'t understand,',
    'please submit an issue to our GitHub repository here and copy the entire log over there:',
    'https://github.com/zold-io/wts.zold.io/issues;',
    'we need your feedback in order to make our system better;',
    'you can also discuss it in our Telegram group: https://t.me/zold_io.'
  ].join(' ')
  "#{user_log.content}\n\n\n#{msg}"
end

get '/remotes' do
  haml :remotes, layout: :layout, locals: merged(
    page_title: '/remotes'
  )
end

def exfee
  settings.toggles.get(known? ? 'exfee-small' : 'exfee', '0.08').to_f
end

def title(suffix = '')
  return 'SANDBOX' if user.fake?
  (user.mobile? ? "+#{user.login}" : "@#{user.login}") + (suffix.empty? ? '' : '/' + suffix)
end

def title_md(u = user)
  if /^[0-9]{6,}$/.match?(u.login)
    "+#{u.login.gsub(/.{3}$/, 'xxx')}"
  else
    "[@#{u.login}](https://github.com/#{u.login})"
  end
end

def anon_ip
  "`#{request.ip.to_s.gsub(/\.[0-9]+$/, '.xx')}` (#{country})"
end

def country(ip = request.ip)
  settings.zache.get("ip_to_country:#{ip}") do
    geo = Geoplugin.new(request.ip, ssl: true, key: settings.config['geoplugin_token'])
    geo.nil? ? '??' : geo.countrycode
  rescue StandardError
    '??'
  end
end

def flash(uri, msg, error: false)
  cookies[:flash_msg] = msg
  cookies[:flash_color] = error ? 'firebrick' : 'seagreen'
  redirect(uri, error ? 303 : 302) unless params[:noredirect]
  msg
end

def context
  "#{request.ip} #{request.user_agent} #{WTS::VERSION}"
end

def merged(hash = {})
  out = @locals.merge(hash)
  out[:local_assigns] = out
  if cookies[:flash_msg]
    out[:flash_msg] = cookies[:flash_msg]
    cookies.delete(:flash_msg)
  end
  out[:flash_color] = cookies[:flash_color] || 'darkgreen'
  cookies.delete(:flash_color)
  out
end

def user_log(u = user.login)
  WTS::FileLog.new(File.join(settings.root, ".zold-wts/logs/#{u}"))
end

def user(login = @locals[:guser])
  raise WTS::UserError, 'E173: You have to login first' unless login
  WTS::User.new(
    login, WTS::Item.new(login, settings.pgsql, log: user_log(login)),
    settings.wallets, log: user_log(login)
  )
end

def confirmed_user(login = @locals[:guser])
  u = user(login)
  raise WTS::UserError, "E174: You, #{login}, have to confirm your keygap first" unless u.confirmed?
  u
end

# This user is known as Zerocracy contributor.
def known?(login = @locals[:guser])
  return false unless login
  return true if %w[yegor256 davvd].include?(login)
  return true if ENV['RACK_ENV'] == 'test'
  return true if login == settings.config['rewards']['login']
  return true if login == settings.config['exchange']['login']
  return true if settings.config['kyc'].include?(login)
  settings.zache.get("#{login}_known?", lifetime: 5 * 60) do
    code = Zold::Http.new(
      uri: Iri.new('https://www.0crat.com/known/').append(login.downcase).to_s
    ).get(timeout: 16).code
    case code
    when 200
      true
    when 404
      false
    else
      raise WTS::UserError, "E226: Something is wrong with 0crat.com, HTTP code is #{code}"
    end
  end
end

# This user is identified in Zerocracy.
def kyc?(login = @locals[:guser])
  return false unless login
  return true if ENV['RACK_ENV'] == 'test'
  return true if login == settings.config['rewards']['login']
  return true if login == settings.config['exchange']['login']
  return true if settings.config['kyc'].include?(login)
  settings.zache.get("#{login}_kyc?", lifetime: 5 * 60) do
    res = Zold::Http.new(
      uri: Iri.new('https://www.0crat.com/known/').append(login.downcase).to_s
    ).get(timeout: 16)
    res.code == 200 && Zold::JsonPage.new(res.body).to_hash['identified']
  end
end

def keygap
  gap = params[:keygap]
  raise WTS::UserError, 'E175: Keygap is required' if gap.nil?
  begin
    confirmed_user.item.key(gap).to_s
  rescue StandardError => e
    raise WTS::UserError, "E176: This doesn\'t seem to be a valid keygap: '#{'*' * gap.length}' (#{e.class.name})"
  end
  gap
end

def latch(login = @locals[:guser])
  File.join(settings.root, "latch/#{login}")
end

def network
  ENV['RACK_ENV'] == 'test' ? 'test' : 'zold'
end

def ops(u = user, log: user_log(u.login))
  WTS::Ops.new(
    u.item,
    u,
    settings.wallets,
    settings.remotes,
    settings.copies,
    log: log,
    network: network
  )
end

# Make sure these features are enabled and let the execution
# continue. If at least one of them is prohibited in the Toggles,
# an exception will be raised.
def features(*list)
  return if @locals[:guser] && vip?
  list.each do |f|
    next unless settings.toggles.get("stop:#{f}", 'no') == 'yes'
    raise WTS::UserError, "E177: This feature \"#{f}\" is temporarily disabled, sorry"
  end
end

def safe_md(txt)
  txt.gsub(/[_*`]/, ' ')
end

def vip?(login = user.login)
  return true if ENV['RACK_ENV'] == 'test'
  return true if login == 'yegor256'
  settings.toggles.get('vip').split(',').include?(login.downcase)
end

def job_link(jid)
  "the full log is [here](https://wts.zold.io/output?id=#{jid})"
end

require_relative 'front/front_auto_pull'
require_relative 'front/front_bonuses'
require_relative 'front/front_btc'
require_relative 'front/front_admin'
require_relative 'front/front_callbacks'
require_relative 'front/front_errors'
require_relative 'front/front_jobs'
require_relative 'front/front_login'
require_relative 'front/front_migrate'
require_relative 'front/front_misc'
require_relative 'front/front_pay'
require_relative 'front/front_paypal'
require_relative 'front/front_push'
require_relative 'front/front_quick'
require_relative 'front/front_rate'
require_relative 'front/front_receipt'
require_relative 'front/front_start'
require_relative 'front/front_toggles'
require_relative 'front/front_upwork'
require_relative 'front/helpers'