@Override
  public String deletePost(DeletePostModel model, ProgressListener listener, CancellableTask task)
      throws Exception {
    String url = getUsingUrl() + "board.php";

    List<NameValuePair> pairs = new ArrayList<NameValuePair>();
    pairs.add(new BasicNameValuePair("board", model.boardName));
    pairs.add(new BasicNameValuePair("post[]", model.postNumber));
    if (model.onlyFiles) pairs.add(new BasicNameValuePair("fileonly", "on"));
    pairs.add(new BasicNameValuePair("postpassword", model.password));
    pairs.add(new BasicNameValuePair("deletepost", "Удалить"));

    HttpRequestModel request =
        HttpRequestModel.builder()
            .setPOST(new UrlEncodedFormEntityHC4(pairs, "UTF-8"))
            .setNoRedirect(true)
            .build();
    String result =
        HttpStreamer.getInstance()
            .getStringFromUrl(url, request, httpClient, listener, task, false);
    if (result.contains("Неправильный пароль")) throw new Exception("Неправильный пароль");
    Matcher errorMatcher = ERROR_POSTING.matcher(result);
    if (errorMatcher.find()) throw new Exception(errorMatcher.group(1));
    return null;
  }
 @Override
 public CaptchaModel getNewCaptcha(
     String boardName, String threadNumber, ProgressListener listener, CancellableTask task)
     throws Exception {
   HttpRequestModel requestModel = HttpRequestModel.builder().setGET().build();
   String html =
       HttpStreamer.getInstance()
           .getStringFromUrl(
               getUsingUrl() + "captcha_update.php", requestModel, httpClient, null, task, false);
   Matcher matcher = CAPTCHA_KEY.matcher(html);
   if (matcher.find()) {
     String captchaUrl = getUsingUrl() + "simple-php-captcha.php?_CAPTCHA&t=" + matcher.group(1);
     Bitmap captchaBitmap = null;
     HttpResponseModel responseModel =
         HttpStreamer.getInstance()
             .getFromUrl(captchaUrl, requestModel, httpClient, listener, task);
     try {
       InputStream imageStream = responseModel.stream;
       captchaBitmap = BitmapFactory.decodeStream(imageStream);
     } finally {
       responseModel.release();
     }
     CaptchaModel captchaModel = new CaptchaModel();
     captchaModel.type = CaptchaModel.TYPE_NORMAL_DIGITS;
     captchaModel.bitmap = captchaBitmap;
     return captchaModel;
   } else throw new Exception("Captcha update epic fail");
 }
  protected ThreadModel[] readWakabaPage(
      String url,
      ProgressListener listener,
      CancellableTask task,
      boolean checkModified,
      UrlPageModel urlModel)
      throws Exception {
    HttpResponseModel responseModel = null;
    WakabaReader in = null;
    HttpRequestModel rqModel =
        HttpRequestModel.builder()
            .setGET()
            .setCheckIfModified(checkModified)
            .setNoRedirect(wakabaNoRedirect())
            .build();
    try {
      responseModel =
          HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, listener, task);
      if (responseModel.statusCode == 200) {
        in = getWakabaReader(responseModel.stream, urlModel);
        if (task != null && task.isCancelled()) throw new Exception("interrupted");
        return in.readWakabaPage();
      } else {
        if (responseModel.notModified()) return null;

        if (canCloudflare()) {
          String html = null;
          try {
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
            IOUtils.copyStream(responseModel.stream, byteStream);
            html = byteStream.toString("UTF-8");
          } catch (Exception e) {
          }
          if (html != null) {
            if (responseModel.statusCode == 403 && html.contains("CAPTCHA")) {
              throw CloudflareException.withRecaptcha(
                  CLOUDFLARE_RECAPTCHA_KEY,
                  getUsingUrl() + CLOUDFLARE_RECAPTCHA_CHECK_URL_FMT,
                  CLOUDFLARE_COOKIE_NAME,
                  getChanName());
            } else if (responseModel.statusCode == 503 && html.contains("Just a moment...")) {
              throw CloudflareException.antiDDOS(url, CLOUDFLARE_COOKIE_NAME, getChanName());
            }
          }
        }
        throw new HttpWrongStatusCodeException(
            responseModel.statusCode,
            responseModel.statusCode + " - " + responseModel.statusReason);
      }
    } catch (Exception e) {
      if (responseModel != null) HttpStreamer.getInstance().removeFromModifiedMap(url);
      throw e;
    } finally {
      IOUtils.closeQuietly(in);
      if (responseModel != null) responseModel.release();
    }
  }
  @Override
  public String sendPost(SendPostModel model, ProgressListener listener, CancellableTask task)
      throws Exception {
    String url = getUsingUrl() + "board.php";
    ExtendedMultipartBuilder postEntityBuilder =
        ExtendedMultipartBuilder.create()
            .setDelegates(listener, task)
            .addString("board", model.boardName)
            .addString("replythread", model.threadNumber == null ? "0" : model.threadNumber)
            .addString("em", model.sage ? "sage" : model.email)
            .addString("subject", model.subject)
            .addString("message", model.comment)
            .addString("recaptcha_response_field", model.captchaAnswer);
    if (model.attachments != null && model.attachments.length > 0)
      postEntityBuilder.addFile("imagefile", model.attachments[0], model.randomHash);
    else if (model.threadNumber == null) postEntityBuilder.addString("nofile", "on");

    postEntityBuilder.addString("postpassword", model.password);

    HttpRequestModel request =
        HttpRequestModel.builder().setPOST(postEntityBuilder.build()).setNoRedirect(true).build();
    HttpResponseModel response = null;
    try {
      response = HttpStreamer.getInstance().getFromUrl(url, request, httpClient, null, task);
      if (response.statusCode == 302) {
        for (Header header : response.headers) {
          if (header != null && HttpHeaders.LOCATION.equalsIgnoreCase(header.getName())) {
            return fixRelativeUrl(header.getValue());
          }
        }
      } else if (response.statusCode == 200) {
        ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
        IOUtils.copyStream(response.stream, output);
        String htmlResponse = output.toString("UTF-8");
        Matcher errorMatcher = ERROR_POSTING.matcher(htmlResponse);
        if (errorMatcher.find()) throw new Exception(errorMatcher.group(1).trim());
      } else throw new Exception(response.statusCode + " - " + response.statusReason);
    } finally {
      if (response != null) response.release();
    }
    return null;
  }
  @Override
  public String reportPost(DeletePostModel model, ProgressListener listener, CancellableTask task)
      throws Exception {
    String url = getUsingUrl() + "board.php";

    List<NameValuePair> pairs = new ArrayList<NameValuePair>();
    pairs.add(new BasicNameValuePair("board", model.boardName));
    pairs.add(new BasicNameValuePair("post[]", model.postNumber));
    if (model.onlyFiles) pairs.add(new BasicNameValuePair("fileonly", "on"));
    pairs.add(new BasicNameValuePair("reportreason", model.reportReason));
    pairs.add(new BasicNameValuePair("reportpost", "Аминь"));

    HttpRequestModel request =
        HttpRequestModel.builder()
            .setPOST(new UrlEncodedFormEntityHC4(pairs, "UTF-8"))
            .setNoRedirect(true)
            .build();
    String result =
        HttpStreamer.getInstance()
            .getStringFromUrl(url, request, httpClient, listener, task, false);
    if (result.contains("Post successfully reported")) return null;
    throw new Exception(result);
  }
  @Override
  public void handle(final Activity activity, final CancellableTask task, final Callback callback) {
    try {
      final HttpClient httpClient =
          MainApplication.getInstance().getChanModule(chanName).getHttpClient();
      final String usingURL = scheme + RECAPTCHA_FALLBACK_URL + publicKey;
      Header[] customHeaders = new Header[] {new BasicHeader(HttpHeaders.REFERER, usingURL)};
      String htmlChallenge =
          HttpStreamer.getInstance()
              .getStringFromUrl(
                  usingURL,
                  HttpRequestModel.builder().setGET().setCustomHeaders(customHeaders).build(),
                  httpClient,
                  null,
                  task,
                  false);

      Matcher challengeMatcher =
          Pattern.compile("name=\"c\" value=\"([\\w-]+)").matcher(htmlChallenge);
      if (challengeMatcher.find()) {
        final String challenge = challengeMatcher.group(1);
        HttpResponseModel responseModel =
            HttpStreamer.getInstance()
                .getFromUrl(
                    scheme + RECAPTCHA_IMAGE_URL + challenge + "&k=" + publicKey,
                    HttpRequestModel.builder().setGET().build(),
                    httpClient,
                    null,
                    task);
        try {
          InputStream imageStream = responseModel.stream;
          final Bitmap challengeBitmap = BitmapFactory.decodeStream(imageStream);

          final String message;
          Matcher messageMatcher =
              Pattern.compile("imageselect-message(?:.*?)>(.*?)</div>").matcher(htmlChallenge);
          if (messageMatcher.find()) message = messageMatcher.group(1).replaceAll("<[^>]*>", "");
          else message = null;

          final Bitmap candidateBitmap;
          Matcher candidateMatcher =
              Pattern.compile(
                      "fbc-imageselect-candidates(?:.*?)src=\"data:image/(?:.*?);base64,([^\"]*)\"")
                  .matcher(htmlChallenge);
          if (candidateMatcher.find()) {
            Bitmap bmp = null;
            try {
              byte[] imgData = Base64.decode(candidateMatcher.group(1), Base64.DEFAULT);
              bmp = BitmapFactory.decodeByteArray(imgData, 0, imgData.length);
            } catch (Exception e) {
            }
            candidateBitmap = bmp;
          } else candidateBitmap = null;

          activity.runOnUiThread(
              new Runnable() {
                final int maxX = 3;
                final int maxY = 3;
                final boolean[] isSelected = new boolean[maxX * maxY];

                @SuppressLint("InlinedApi")
                @Override
                public void run() {
                  LinearLayout rootLayout = new LinearLayout(activity);
                  rootLayout.setOrientation(LinearLayout.VERTICAL);

                  if (candidateBitmap != null) {
                    ImageView candidateView = new ImageView(activity);
                    candidateView.setImageBitmap(candidateBitmap);
                    int picSize =
                        (int) (activity.getResources().getDisplayMetrics().density * 50 + 0.5f);
                    candidateView.setLayoutParams(new LinearLayout.LayoutParams(picSize, picSize));
                    candidateView.setScaleType(ImageView.ScaleType.FIT_XY);
                    rootLayout.addView(candidateView);
                  }

                  if (message != null) {
                    TextView textView = new TextView(activity);
                    textView.setText(message);
                    textView.setTextAppearance(activity, android.R.style.TextAppearance);
                    textView.setLayoutParams(
                        new LinearLayout.LayoutParams(
                            LinearLayout.LayoutParams.MATCH_PARENT,
                            LinearLayout.LayoutParams.WRAP_CONTENT));
                    rootLayout.addView(textView);
                  }

                  FrameLayout frame = new FrameLayout(activity);
                  frame.setLayoutParams(
                      new LinearLayout.LayoutParams(
                          LinearLayout.LayoutParams.MATCH_PARENT,
                          LinearLayout.LayoutParams.WRAP_CONTENT));

                  final ImageView imageView = new ImageView(activity);
                  imageView.setLayoutParams(
                      new FrameLayout.LayoutParams(
                          FrameLayout.LayoutParams.MATCH_PARENT,
                          FrameLayout.LayoutParams.MATCH_PARENT));
                  imageView.setScaleType(ImageView.ScaleType.FIT_XY);
                  imageView.setImageBitmap(challengeBitmap);
                  frame.addView(imageView);

                  final LinearLayout selector = new LinearLayout(activity);
                  selector.setLayoutParams(
                      new FrameLayout.LayoutParams(
                          FrameLayout.LayoutParams.MATCH_PARENT,
                          FrameLayout.LayoutParams.MATCH_PARENT));
                  AppearanceUtils.callWhenLoaded(
                      imageView,
                      new Runnable() {
                        @Override
                        public void run() {
                          selector.setLayoutParams(
                              new FrameLayout.LayoutParams(
                                  imageView.getWidth(), imageView.getHeight()));
                        }
                      });
                  selector.setOrientation(LinearLayout.VERTICAL);
                  selector.setWeightSum(maxY);
                  for (int y = 0; y < maxY; ++y) {
                    LinearLayout subSelector = new LinearLayout(activity);
                    subSelector.setLayoutParams(
                        new LinearLayout.LayoutParams(
                            LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
                    subSelector.setOrientation(LinearLayout.HORIZONTAL);
                    subSelector.setWeightSum(maxX);
                    for (int x = 0; x < maxX; ++x) {
                      FrameLayout switcher = new FrameLayout(activity);
                      switcher.setLayoutParams(
                          new LinearLayout.LayoutParams(
                              0, LinearLayout.LayoutParams.MATCH_PARENT, 1f));
                      switcher.setTag(new int[] {x, y});
                      switcher.setOnClickListener(
                          new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                              int[] coord = (int[]) v.getTag();
                              int index = coord[1] * maxX + coord[0];
                              isSelected[index] = !isSelected[index];
                              v.setBackgroundColor(
                                  isSelected[index]
                                      ? Color.argb(128, 0, 255, 0)
                                      : Color.TRANSPARENT);
                            }
                          });
                      subSelector.addView(switcher);
                    }
                    selector.addView(subSelector);
                  }

                  frame.addView(selector);
                  rootLayout.addView(frame);

                  Button checkButton = new Button(activity);
                  checkButton.setLayoutParams(
                      new LinearLayout.LayoutParams(
                          LinearLayout.LayoutParams.MATCH_PARENT,
                          LinearLayout.LayoutParams.WRAP_CONTENT));
                  checkButton.setText(android.R.string.ok);
                  rootLayout.addView(checkButton);

                  ScrollView dlgView = new ScrollView(activity);
                  dlgView.addView(rootLayout);

                  final Dialog dialog = new Dialog(activity);
                  dialog.setTitle("Recaptcha");
                  dialog.setContentView(dlgView);
                  dialog.setCanceledOnTouchOutside(false);
                  dialog.setOnCancelListener(
                      new DialogInterface.OnCancelListener() {
                        @Override
                        public void onCancel(DialogInterface dialog) {
                          if (!task.isCancelled()) {
                            callback.onError("Cancelled");
                          }
                        }
                      });
                  dialog
                      .getWindow()
                      .setLayout(
                          ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                  dialog.show();

                  checkButton.setOnClickListener(
                      new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                          dialog.dismiss();
                          if (task.isCancelled()) return;
                          PriorityThreadFactory.LOW_PRIORITY_FACTORY
                              .newThread(
                                  new Runnable() {
                                    @Override
                                    public void run() {
                                      try {
                                        List<NameValuePair> pairs = new ArrayList<NameValuePair>();
                                        pairs.add(new BasicNameValuePair("c", challenge));
                                        for (int i = 0; i < isSelected.length; ++i)
                                          if (isSelected[i])
                                            pairs.add(
                                                new BasicNameValuePair(
                                                    "response", Integer.toString(i)));

                                        HttpRequestModel request =
                                            HttpRequestModel.builder()
                                                .setPOST(
                                                    new UrlEncodedFormEntityHC4(pairs, "UTF-8"))
                                                .setCustomHeaders(
                                                    new Header[] {
                                                      new BasicHeader(HttpHeaders.REFERER, usingURL)
                                                    })
                                                .build();
                                        String response =
                                            HttpStreamer.getInstance()
                                                .getStringFromUrl(
                                                    usingURL,
                                                    request,
                                                    httpClient,
                                                    null,
                                                    task,
                                                    false);
                                        String hash = "";
                                        Matcher matcher =
                                            Pattern.compile(
                                                    "fbc-verification-token(?:.*?)<textarea[^>]*>([^<]*)<",
                                                    Pattern.DOTALL)
                                                .matcher(response);
                                        if (matcher.find()) hash = matcher.group(1);

                                        if (hash.length() > 0) {
                                          Recaptcha2solved.push(publicKey, hash);
                                          activity.runOnUiThread(
                                              new Runnable() {
                                                @Override
                                                public void run() {
                                                  callback.onSuccess();
                                                }
                                              });
                                        } else {
                                          throw new RecaptchaException(
                                              "incorrect answer (hash is empty)");
                                        }
                                      } catch (final Exception e) {
                                        Logger.e(TAG, e);
                                        if (task.isCancelled()) return;
                                        handle(activity, task, callback);
                                      }
                                    }
                                  })
                              .start();
                        }
                      });
                }
              });
        } finally {
          responseModel.release();
        }
      } else throw new Exception("can't parse recaptcha challenge answer");
    } catch (final Exception e) {
      Logger.e(TAG, e);
      if (!task.isCancelled()) {
        activity.runOnUiThread(
            new Runnable() {
              @Override
              public void run() {
                callback.onError(e.getMessage() != null ? e.getMessage() : e.toString());
              }
            });
      }
    }
  }