laravelのfactoryでコメントとコメントの子的なのを作成する

どんなの作るか

ブログのコメントとかでよくある 記事のコメント コメントのコメント とかできるやつ

コード

Factory

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Comment;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Comment>
 */
class CommentFactory extends Factory
{
    private static int $sequence = 1;
    private static int $user_id = 1;
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'user_id' => fn () => self::$sequence++,
            'question_id' => fake()->numberBetween(1,100),
            'parent_id' => NULL,
            'body' => fake()->realText($maxNbChars =100),
        ];
    }

    public function child(): static
    {
        return $this->state(fn (array $attributes) => [
            'parent_id' => Comment::all()->random(),
        ]);
    }

    public function configure()
    {
        return $this->afterMaking(function (Comment $comment) {
            //
        })->afterCreating(function (Comment $comment) {
            if (self::$user_id !== $comment->user_id) {
                self::$user_id = $comment->user_id;
                self::$sequence = 1;
            }
        });
    }
}

Seeder

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Comment;


class CommentSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Comment::factory()
            ->count(1000)
            ->create();
        for ($i = 0; $i < 3; $i++) {
            Comment::factory()
                ->count(1000)
                ->child()
                ->create();
        }
    }
}

こうすれば
1000個のユーザーで
1000件の記事へのコメント
1000件*3のコメントへのコメントが作成できる
各ユーザーは4回書き込んだことになる

参考

Laravel Factoryで単純に連番を振ったり〇〇単位で連番を振る方法 │ wonwon eater

Eloquent:ファクトリ 10.x Laravel

google workspaceでドメインの所有権の証明ができない

症状

Gmailを有効にするところからやるといつまで立ってもできなかった
以下のようなエラーが出る

Gmail を有効にできませんでした。以下の点にご対応のうえ、もう一度お試しください。
デバイスがありません

よくわからんがこれでメールの送受信はできる

https://admin.google.com/u/0/ac/signup/setup/v2/verify/txt

ここからTXTレコードで証明する方法をやる
これは一瞬で終わった

この後にgmailを有効にするページから
https://admin.google.com/u/5/ac/signup/setup/v2/gettingstarted
同じようにMXレコード登録する
エラーが出るけど、この状態でメールの送受信はできる

そして半日が過ぎたら勝手に証明終わってた

https://toolbox.googleapps.com/apps/dig/

googleのdig使って反映されているのを確認してたけど、これに反映されているからといってすぐできるものでもなかったらしい

vscodeのvimで、gg=Gでインデントできない

環境

formatterにLaravel Blade formatterを使用してblade.phpをインデントしようとしている

事象

vscodevimで全体をインデントしようと

gg=G

を入力したら

Configure Default Formatter
Extension 'Laravel Blade Formatter' is configured as formatter but it cannot format 'Blade'-files

と出て、インデントできない

わかったこと

Ctrl+Shift+p

で出てくるところに以下の文を打ち込むとできる場合とできない場合がある

インデント可能
editor.action.formatDocument

インデント不可
editor.action.formatSelection

今後

とりあえずインデント可能なやつのショートカットキーは

Alt+Shift+f

なのでしばらくこれで過ごす

選択したものをインデントするときの処理でなんかだめっぽい Laravel Blade formatter側がだめな気がする

ちなみに

extensionのvimで呼び出している箇所は

https://github.com/VSCodeVim/Vim/blob/master/src/actions/operator.ts

だと思う

@RegisterAction
class FormatOperator extends BaseOperator {
  public keys = ['='];
  public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine, Mode.VisualBlock];

  public async run(vimState: VimState, start: Position, end: Position): Promise<void> {
    // = operates on complete lines
    vimState.editor.selection = new vscode.Selection(start.getLineBegin(), end.getLineEnd());
    await vscode.commands.executeCommand('editor.action.formatSelection');
    let line = vimState.cursorStartPosition.line;

    if (vimState.cursorStartPosition.isAfter(vimState.cursorStopPosition)) {
      line = vimState.cursorStopPosition.line;
    }

    const newCursorPosition = TextEditor.getFirstNonWhitespaceCharOnLine(vimState.document, line);
    vimState.cursorStopPosition = newCursorPosition;
    vimState.cursorStartPosition = newCursorPosition;
    await vimState.setCurrentMode(Mode.Normal);
  }
}

画像の2点をクリックしてcropした画像と座標を手に入れるpython

前書き

画像の切り抜きとそれの座標がほしかった

使い方

クリップボードに画像があればそのまま実行
もしくは画像を実行ファイルにD&D

実際に書いたもの

from PIL import Image, ImageTk, ImageGrab
import numpy as np
import cv2
import os
import sys

class img2crop():
    def __init__(self):
        os.chdir(os.path.dirname(os.path.abspath(__file__)))
        self.x1=self.y1=self.x2=self.y2=self.f=0
        if len(sys.argv) <= 1:
            self.paste_clipboard()
        else:
            self.open_img()

    def pil2cv(self,image):
        ''' PIL型 -> OpenCV型 '''
        new_image = np.array(image, dtype=np.uint8)
        if new_image.ndim == 2:  # モノクロ
            pass
        elif new_image.shape[2] == 3:  # カラー
            new_image = new_image[:, :, ::-1]
        elif new_image.shape[2] == 4:  # 透過
            new_image = new_image[:, :, [2, 1, 0, 3]]
        return new_image

    def onMouse(self,event, x, y, flags, params):
        if event == cv2.EVENT_LBUTTONDOWN:
            if not self.f:
                self.x1=x
                self.y1=y
                self.f=True
            else:
                self.x2=x
                self.y2=y
                if (self.x1 >= self.x2 and self.y1 >= self.y2):
                    t = self.x1
                    self.x1 = self.x2
                    self.x2 = t
                    t = self.y1
                    self.y1 = self.y2
                    self.y2 = t
                
                # 出力文字列
                print(f'{self.x1},{self.y1},{self.x2},{self.y2}')

                self.f=False
                cv2.imshow(f'img2crop - {self.x1},{self.y1},{self.x2},{self.y2}', self.cv_im[self.y1:self.y2,self.x1:self.x2])
                cv2.imwrite(f'{self.x1},{self.y1},{self.x2},{self.y2}.png', self.cv_im[self.y1:self.y2,self.x1:self.x2])

    def paste_clipboard(self):
        self.im = ImageGrab.grabclipboard()
        if isinstance(self.im, Image.Image):
            self.view_im()
        else:
            print(u"クリップボードは画像ではありません")
            input()

    def open_img(self):
        try:
            self.im = Image.open(sys.argv[1])
            self.view_im()
        except:
            print(u"ファイルは画像ではありません")
            input()

    def view_im(self):
        self.cv_im = self.pil2cv(self.im)
        cv2.imshow('img2crop', self.cv_im)
        cv2.setMouseCallback('img2crop', self.onMouse)
        cv2.waitKey(0)

if __name__ == '__main__':
    img2crop()

nicehashの出金手数料を通知してくれるdiscord botをgasで書いた

前書き

出金手数料通知してくれるdiscordサーバがあったので入っていたのですが、通知してくれなくなったので自分で書いた
サーバは公開したくないのでソースコードだけ貼っときます

ソースコード

  // ここの箇所は各自変更する必要あり
  const discordWebHookURL = "ディスコードのウェブフックURLをコピペするところ";

https://gist.githubusercontent.com/silenvx/100159c5931271cbb694a3d9fd49b0b0/raw/44917dbef5ef7586181b8936596d4073db06ddfd/%25E3%2582%25B3%25E3%2583%25BC%25E3%2583%2589.gs

// nicehashの手数料を取得
function get_nicehash_fee(coin='BTC'){
  var response = UrlFetchApp.fetch('https://api2.nicehash.com/main/api/v2/public/service/fee/info')
  var fee_info = JSON.parse(response.getContentText());
  return(fee_info['withdrawal']['BITGO']['rules'][coin]['intervals'][0]['element']['sndValue'])
}

// discordにウェブフックを使ってbotメッセージを送信
function send_discord(msg='', everyone = false){
  const discordWebHookURL = "ディスコードのウェブフックURLをコピペするところ";
  if(everyone){
    msg = '@everyone' + msg
  }
  const message = {
    "content": msg,
    "tts": false
  }
  const param = {
    "method": "POST",
    "headers": { 'Content-type': "application/json" },
    "payload": JSON.stringify(message)
  }
  UrlFetchApp.fetch(discordWebHookURL, param);
}

// 現在の時刻の文字列を返す
function get_now(){
  var date = new Date();
  return Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
}

// main
function myFunction() {
  // 取得するコインの種類
  var coin = "BTC"
  // 前回の手数料
  var properties = PropertiesService.getScriptProperties();
  var fee_before = properties.getProperty("fee_before");

  var res_fee = get_nicehash_fee(coin);

  // 表示するメッセージの生成
  msg = '['+get_now()+'] '+coin+': '+res_fee;
  Logger.log(msg + ', before:' + fee_before);

  // 前回と違えば表示する
  if(fee_before != res_fee){
    send_discord(msg, everyone = res_fee <= 0.000001);
  }
  // 前回の手数料を保存
  properties.setProperty("fee_before", res_fee);
}

使い方

1.gasを作成

googleドライブを開いて、新規 -> その他 -> Google Apps Script
ここに先程のソースコード貼り付ける

2.ウェブフックURLの取得と書き換え

導入したいdiscordサーバの設定 -> 連携サービス -> ウェブフック -> 新しいウェブフック -> ウェブフックURLをコピー
これで得たURLを先程の貼り付けたソースコードの変更する必要のある場所を書き換える

3.トリガーの設定をして実行

gasでトリガー -> トリガーを追加 を選んで、実行する関数は「myFunction」
分ベースのタイマー、1分おきとかその辺は好みで設定して完了

動作について

前回と違う手数料に変わった時だけdiscordに通知されます
手数料が0.000001BTC以下だと@everyoneをつけて通知します

discordなんて使いたくねーよ

なんて人がいたらpythonで1分ごとに取得するものをgasで書く前に試しに書いたのでどうぞ
0.000001BTC以下だったらビープ音鳴らすなりしたらいいと思う(そこまで書いてません)

import json
import urllib.request
import datetime
import time

while True:
    url = 'https://api2.nicehash.com/main/api/v2/public/service/fee/info'
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as res:
        dt_now = datetime.datetime.now()
    
        fee_info = json.load(res)
        coin = 'BTC'
        print("[{}] {}: {:.8f}".format(dt_now.strftime('%Y-%m-%d %H:%M:%S'), coin, fee_info['withdrawal']['BITGO']['rules'][coin]['intervals'][0]['element']['sndValue']))
    
    time.sleep(60)

discordのアクティビティステータス認証済みを削除したり名前を変える

追記

デベロッパーツールは
%appdata%/discord/settings.json
に以下の文を書き足さないと開けなくなりました

  "DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true

やり方

discordを開いた状態でCtrl+Shift+Iを押してデベロッパーツールを起動する
F1キーを押して設定を開き、Preferencesの右下の方にあるEnable Local Overridesを有効にする
Networkタブを開きCtrl+Rでリロードする
その中からdetectableを探し出し保存し、Request URLもメモする
自分の場合は

https://discordapp.com/api/v9/applications/detectable

Local Overridesに使うフォルダを作成し、先程のRequest URLのドメインからのPATHと同じようなフォルダ階層を作成する

D:\discord_local_overrides\discordapp.com\api\v9\applications

このapplicationsの中に先ほどダウンロードしたdetectableを保存する
拡張子がついている場合は削除してファイル名を「detectable」だけにする
discordの画面に戻りSourcesタブ>Overridesタブ>+Select folder for overridesからLocal Overridesに使うフォルダを指定する
先程の例でいうと

D:\discord_local_overrides

Overridesタブが見当たらない場合は「>>」ボタンを押すと出てくる
追加したのに変化が見られない場合はデベロッパーツールを閉じて開いてやると出てくる(Ctrl+Shift+Iを2回)

detectableを編集
全ての認証済みをなくす
[]

だけにする
するとdiscord内で名前変更可能になるが、アイコンは表示されなくなる

名前を変更する

表示される名前で検索してヒットしたところを変更する
具体的な場所については下に記載

detectableの例
  {
    "description": "A popular mobile collectible card game now also on PC.",
    "developers": [
      {
        "id": "521816528895737856",
        "name": "Cygames, Inc."
      }
    ],
    "executables": [
      {
        "is_launcher": false,
        "name": "shadowverse.exe",
        "os": "win32"
      }
    ],
    "hook": true,
    "icon": "748e96fabbfc42013d1806fe220f891d",
    "id": "363409615620407316",
    "name": "名前を変更する場所",
    "publishers": [
      {
        "id": "521816528895737856",
        "name": "Cygames, Inc."
      }
    ],
    "splash": "d72314c7581c59293066459ed7baf8cf",
    "summary": "A popular mobile collectible card game now also on PC.",
    "third_party_skus": [
      {
        "distributor": "steam",
        "id": "453480",
        "sku": "453480"
      }
    ],
    "verify_key": "6ee784f97ba25886290820169f26c9432bbde09a226b559c6749fca4684db476"
  },

1部のゲームだけ消したい場合はこれごと削除

後書き

取得urlが変わることがあるので注意
その場合はフォルダ名の変更の必要あり
アイコンの変更もできたが再現性がなかったためよくわからなかった
認証済みゲームなのかの認識は実行ファイル名でしているので、全く関係ないゲームを、例えば「shadowverse.exe」にして実行するとshadowverseとして認識できる

javascriptの無名関数で定義されちゃったEventListenerを消す

前書き

あるサイトで右クリックを使いたかったけど右クリックが前ページに戻るって動作になっててコンテキストメニューが表示されなかった
調べると、無名関数で登録されてるせいで

element.removeEventListener("contextmenu", ここ);

ここのところが指定できなくて困ったのでなんとかする
たぶん、chromeでしか使えない

サンプルコード

var x = document.getElementsByClassName("reader")[0];
x.removeEventListener("contextmenu", getEventListeners(x).contextmenu[0].listener)

説明

1行目は、右クリックしたい要素を変数にいれる
2行目は、さっき「ここ」って書いた場所に書いてる奴

getEventListeners(x).contextmenu[0].listener

が無名関数を特定する処理