canvasに表示した画像を丸で囲って表示する
やりたいこと
canvasにdrawImage()で貼り付けた画像を正円で表示させたい(丸型にトリミングされたように表示させたい)
やるまでは「quadraticCurveTo()
でベジェ曲線使ってやらなかん感じ〜!?!?!?!数学できないよ〜!!」(数学関係あるかもわからない)って思ってたけど全然使わなかった
実装
一部抜粋
const image = new Image(); image.src = "表示したい画像"; image.onload = () => { ctx.beginPath(); // 半径40pxの正円 ctx.arc(50, 50, 40, 0, 2 * Math.PI); ctx.fill(); // 枠線表示したい場合はctx.stroke();にする // パスで切り抜く ctx.clip(); // 80x80の画像を表示する ctx.drawImage(image, 10, 10, 80, 80); // 他の描画に影響が出ないようにrestoreする ctx.restore(); };
コード全文
canvasで正円を描画したいのに楕円になってしまう
Reactでcanvas扱ってたときに正円を描画したいのに楕円になったときの備忘録
canvas描画部分
const ctx = this.canvas.current?.getContext('2d'); if (!ctx) { return; } ctx.fillStyle = "black"; ctx.fillRect(30, 0, 290, 450); ctx.fillStyle = "orange"; ctx.beginPath(); ctx.arc(100, 50, 15, 0, 2 * Math.PI); ctx.fill();
修正前
<canvas style={{ width: "320px", height: "450px" }} ref={this.canvas}></canvas>
修正後
<canvas width="320px" height="450px" ref={this.canvas}></canvas>
原因はstyle属性にwidth
,height
を書いていたこと
canvasにwidth
, height
属性として記述すればOK
意図的に楕円を描きたいときはelipseを使う
参考
API Blueprintでパラメータにハイフン(-)が含まれる場合にハイフン以降が出力されない
はじめに
API Blueprint
でクエリパラメータをつけるエンドポイントを定義したかったけど、出力されたフォーマットが想定していたものと違っていた時の備忘録
修正前
http://example.com/api/example?date=2000-01-10
みたいなURIを想定して、以下のように書いた(一部抜粋)
## サンプル [/api/example{?date}] ### 取得 [GET] + Parameters + date: 2000-01-10 (string) - 対象年月日
aglio
を使って出力されたもの
2000で切れてる....!!!!
修正後
値をバッククォートで囲ってあげればいい
## サンプル [/api/example{?date}] ### 取得 [GET] + Parameters + date: `2000-01-10` (string) - 対象年月日
ハイフン以降も表示されました
ドキュメントにもちゃんと書いてありました
NOTE: It's important to note, that if a key or value contains reserved characters such as :, (,), <, >, {, }, [, ], _, *, -, +, ` then the value must be wrapped in a code-block using back-ticks.
React Nativeでフォアグラウンドを検知する
はじめに
アプリがフォアグラウンドにあるタイミングで特定の処理を実施したかった
検証環境
$ react-native -v react-native-cli: 2.0.1 react-native: 0.67.3
コード
import React from 'react'; import { AppState } from 'react-native'; class App extends React.Component { componentDidMount() { AppState.addEventListener('change', () => { if (AppState.currentState === 'active') { // ...フォアグラウントで実施したい処理 } }); }
ちゃんとReactNative側に用意されててよかった...
状態
AppStateStatus | トリガー |
---|---|
active | アプリがフォアグラウンドに入ったとき。 |
background | アプリがバックグラウンドに入ったとき。 |
inactive | iOSのみ。アプリを開いた状態で通知センターを表示したとき。タスクキルする手前のタスク一覧画面のとき。 |
着信のときもinactive
になるらしいけど検証してないからわからん。。。
unknown
とextension
はどこで使うねん
参考
Rect Nativeでダークモードを無効にする
検証環境
$ react-native -v react-native-cli: 2.0.1 react-native: 0.67.3
実装
iOS
ios/{project}/AppDelegate.m
を以下のように修正する
@implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 省略 // ↓追加 if (@available(iOS 13, *)) { self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; } return YES; }
※iOS13以下を切るならif文要らないかも
Android
android/app/src/main/java/path/to/project/MainApplication.java
を以下のように修正する
// ↓追加 import androidx.appcompat.app.AppCompatDelegate; // 省略 public class MainApplication extends Application implements ReactApplication { // 省略 @Override public void onCreate() { super.onCreate(); SoLoader.init(this, /* native exopackage */ false); initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); // 追加 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); }
最後に
ダークモード対応してるアプリすごい。
nodeのversionを切り替えるときに `Use of uninitialized value $b1 in numeric comparison (<=>) at ...` と表示される
nodeのversionを切り替えたときに以下のメッセージが表示された
nodebrew use v16.8.0 Use of uninitialized value $b1 in numeric comparison (<=>) at /Users/XXX/.nodebrew/current/bin/nodebrew line 678. Use of uninitialized value $a1 in numeric comparison (<=>) at /Users/XXX/.nodebrew/current/bin/nodebrew line 678. use v16.8.0
ググった感じ、~/.nodebrew/node
に.DS_Store
があるのが原因らしいので消しておく
$ ls -al ~/.nodebrew/node # .DS_Store が表示される $ rm ~/.nodebrew/node/.DS_Store
再度use
でエラーが表示されなければOK
$ nodebrew use v16.8.0 use v16.8.0
俺はuse
で発生したけど普通にlist
とかでもなるっぽい
【GoogleAppsScript】SESAME API を使ってGASから施錠・解錠する
はじめに
この記事は過去に投稿したSlackからNatureRemoのAPIを叩く記事の延長編です。
外出時に電化製品の電源オフったあとに、そのまま鍵の解錠→再施錠を実施するためにSESAME APIを使用します:)
やること
過去記事の延長線なのでSlackとの連携部分は省略します。
1. SESAME APIの準備
https://partners.candyhouse.co/login から SESAME に登録済みのメールアドレスでログイン
ログインしたらメールが来るので認証などを済ませる
https://partners.candyhouse.co/ が閲覧可能になるので施錠・解錠に使用したい登録デバイスをクリック
API キー・SecretKey・UUID を UUID をコピる
2. GASの作成
施錠・解錠には以下の API を使用
RequestBody には以下が必要
①操作コマンド
command | 操作内容 |
---|---|
88 | トグル(施錠状態 → 解錠する・解錠状態 → 施錠する) |
82 | 施錠 |
83 | 解錠 |
②履歴に表示される識別子
アプリの操作履歴↓に表示される文字列を base64 形式で指定する
// 履歴に表示される名前を"GoogleAppsScript"にしたい場合 Utilities.base64Encode("GoogleAppsScript"); // "R29vZ2xlQXBwc1NjcmlwdA==";
最初、Utilities.base64Encode()
があるの知らなくて、ずっと btoa()
使ってたよ...
③AES-CMAC 方式で暗号化したタイムスタンプ
暗号化において、以下の記事を参考にさせていただきました。
AES-CMAC での暗号化には CryptoJS を使用
CryptoJS のうち、以下のファイルを使うので GAS のコード.gs
にコピペ
(参考記事だと別ファイルでのスクリプトになってるけど、呼び出せなかったので全部コード.gs
に書いた...)
SESAME の API 仕様書に書かれてる 1,2,3 の部分は以下だと思う(多分)
// 1. timestamp (SECONDS SINCE JAN 01 1970. (UTC)) // 1621854456905
// 2. timestamp to uint32 (little endian) //f888ab60
// 3. remove most-significant byte //0x88ab60
// 1. timestampのミリ秒を切り捨て const ts = Math.floor(Date.now() / 1000); // ビューの作成 const view = new DataView(new ArrayBuffer(4)); // 2. 32bitにしてリトルエンディアンで格納 view.setUint32(0, ts, true); // 3. hexにして最上位ビットを削除 const src = view.getUint32(0).toString(16).slice(2, 8); // 暗号化 const sign = CryptoJS.CMAC( CryptoJS.enc.Hex.parse(SECRET_KEY), CryptoJS.enc.Hex.parse(src) ).toString();
これを関数にして解錠 →40 秒後に再施錠をする
const CMD = { lock: 82, unlock: 83 }; changeSesameCmd(CMD.unlock); Utilities.sleep(40000); // 40秒後に再施錠する changeSesameCmd(CMD.lock);
コード全文
最後に
【GoogleAppsScript】朝に雨が降っていたら傘を持って帰るのを忘れないようにSlackに教えてもらう
(最近SlackとGASの記事ばっか書いてる気がする...)
はじめに
以下を解消するのが目的です
- 朝に雨が降ってると退勤時に雨が降ってようが降ってまいが、会社の傘立てに傘を忘れる確率が100%(降ってるときは傘立てが事務所にあるから取りに戻る必要がある)
- 傘を一つしか持ってないから2日連続で雨のときに無事死亡する
2つ目はずぶ濡れ出勤すれば済む問題なので1つ目を解消します:)
あと、スマホと傘を常時一緒に持っているわけではないのでスマートタグは使いません
どうしたか
弊社では退勤時、Slackの#timecardに「お先に失礼します。」という投稿をする慣習があります。
その慣習を使って、投稿された「お先に失礼します。」のメッセージのスレッドに傘を持ち帰る旨の投稿をして傘の置き忘れを防ぎます。
チャンネルではなくスレッドに書き込む理由は、投稿された自動メッセージがリモートワークの方など他の社員にとって邪魔な情報になる可能性があるからです。
「お先に失礼します。」に関しての自動化は過去記事に記載していますのでよければ以下もご覧ください :) iliiliiiliili.hatenablog.jp
手順
1. SlackBotの作成
1-1. Event Subscriptionsの設定
https://api.slack.com/apps/からSlackBotを作成します。
この辺りの作成手順はググれば出てくるので割愛します。
メッセージの投稿をトリガーとしてWebhookを使用するのでEvent Subscriptions
をオンにします。
Webhook先のURLをVerified
にするために一旦GASを以下のコードでデプロイしておきます。
const params = JSON.parse(e.postData.getDataAsString()); if (params.challenge) { return ContentService.createTextOutput(params.challenge); }
Request URL
がVerified
になればOK
eventは以下を設定しました。(チャンネルのみで使用するのであればmessage.channels
だけでOK。多分。)
1-2. OAuth & Permissions の設定
次に、傘を忘れないようにするメッセージを投稿するためにOAuth & Permissions
を設定します
今回はメッセージの書き込みとアプリBotの表示のカスタマイズをしたいのでScopes
には以下を設定
アプリBotを使うのでBot User OAuth Token
に表示されているトークンをメモっておく
2. GASの作成
2-1. パラメータの確認
doPost()
でSlackからの投稿を受け取って処理します。
function doPost(e) { if (params.event.user !== SLACK_USER_ID) { return; } if ( params.event.type === "message" && params.event.text === "お先に失礼します。" ) { checkBringBackUmbrella(params.event.ts); } }
やってること
- 自分以外のSlackUserIdの場合は何もしない
- テキストが「お先に失礼します。」の時にのみ天気予報のチェック(+Slackへの投稿)を行う
2-2. 天気予報のAPIを叩く
次に、天気予報のAPIを叩いて当日の雨量を取得します。
天気予報を取得するためのAPIにはOpen-Meteo
を使用しました。
const date = new Date().toLocaleDateString(); // YYYY-MM-DDのformatに整形 const format = date.split("/").map((row) => (row.length === 1 ? "0" + row : row)); const formattedDate = [format[2], format[0], format[1]].join("-"); const LONGTIDE = 雨量を取得したい地域の経度; const LATITUDE = 雨量を取得したい地域の緯度; const res = UrlFetchApp.fetch( "https://api.open-meteo.com/v1/forecast?timezone=Asia/Tokyo&latitude=" + LATITUDE + "&longitude=" + LONGTIDE + "&hourly=rain&start_date=" + formattedDate + "&end_date=" + formattedDate );
(new Date().toLocaleDateString()
の戻りが2022/9/22
じゃなくて9/22/2022
になるのなんでなん...?
ちなGASのタイムゾーンは東京にしてるし、appsscript.json
のtimeZone
もAsia/Tokyo
になってる)
2-3. Slackにメッセージを投稿する
午前7~9時のいずれかの雨量が0.1以上だったら会社に傘を持ってきているとみなして「お先に失礼します。」のメッセージのスレッドに対して投稿する。
(なお、会社用の鞄に常時折りたたみ傘が入ってるので、10時以降に雨が降った場合は傘立てには傘は入っていないものとしています。)
const src = JSON.parse(res); // 7,8,9時の雨量 const morningRains = src.hourly.rain.slice(7, 10); // すべて0.0の場合は雨が降っていなかった if (morningRains.every((rain) => rain === 0.0)) { return; } // 雨が降っている場合はSlackのスレッドに書き込む const SLACK_TOKEN = "SlackのAppBotのトークン"; const SLACK_CHANNEL_NAME = "チャンネル名"; UrlFetchApp.fetch("https://api.slack.com/api/chat.postMessage", { method: "post", payload: { token: SLACK_TOKEN, channel: SLACK_CHANNEL_NAME, text: "<@"+SLACK_USER_ID+">今日は午前中の雨量が0.1以上でした。傘があれば持って帰りましょう。", icon_emoji: ":umbrella:", username: "放置傘警察", thread_ts: ts, reply_broadcast: false, }, });
これで雨のときにはSlackが教えてくれるようになりました:)
コード全文
最後に
会社以外の傘立てに忘れたらおしまいだね!!
参考
【GoogleAppsScript】Slackに投稿したらNature RemoのAPIを使って照明とエアコンを消す
はじめに
外出時にいつも同居人に外出する旨のメッセージを送ってるんだけど↓の3つをやらなくちゃいけないのがめんどい
- Nature Remoで照明を消す
- Nature Remoでエアコンを消す
- Slackでメッセージを送る
「Slackにメッセージを送るのが前提としてあるんだったら他のフローは省略できるのでは...?」って思ったのがきっかけです
Nature Remoとは?
やること
- Slackにメッセージを投稿する
- SlackからGASにイベントを投げてもらう
- GASからNature RemoのAPIを叩く
1は人間が実行して2,3を自動化します:)
使うもの
(いつも GoogleAppsScript なのか GoogleAppScripts なのかが覚えられずググってまう)
手順
1. Slackでアプリbotの作成
https://api.slack.com/apps からアプリBotを作成して、Event Subscriptions
からトリガーとなるイベントを設定する
今回はチャンネルでもDMでも使いたかったから以下を設定
Request URL
で検証するために以下の内容でGASを作成して一旦Webアプリケーションとしてデプロイする
function doPost(e) { const params = JSON.parse(e.postData.getDataAsString()); return ContentService.createTextOutput(params.challenge); }
実行URLをSlack側のRequest URL
に入力
↓みたいにVerified
になったらSave Changes
ボタンを押せるようになるので保存
2. GASからNature RemoのAPIを叩く
https://home.nature.global/ からNature RemoのBearerトークンの発行
電化製品を操作するためのIDの取得には https://api.nature.global/1/appliances/
へGETリクエストを送って確認する
①電気を消す
UrlFetchApp.fetch( "https://api.nature.global/1/appliances/" + LIGHT_ID + "/light", { method: "post", headers: { Authorization: "Bearer " + NATURE_REMO_TOKEN }, payload: { appliance: LIGHT_ID, button: "off", }, } );
②エアコンを消す
UrlFetchApp.fetch( "https://api.nature.global/1/appliances/" + AIRCON_ID + "/aircon_settings", { method: "post", headers: { Authorization: "Bearer " + NATURE_REMO_TOKEN }, payload: { appliance: AIRCON_ID, button: "power-off", }, } );
クエリパラメータでもbodyでも動作するのなんでなん...?
Slackからのイベントでメッセージが特定の言葉だったときにこれらを実行する
function doPost(e) { const SLACK_USER_ID = "自分のSlackUserId"; const params = JSON.parse(e.postData.getDataAsString()); if ( params.event.type === "message" && ["トリガーにしたいメッセージの配列"].includes(params.event.text) && params.event.user === SLACK_USER_ID ) { turnOffAppliances(); return; } return; }
コード全文
最後に
スマートスピーカー持ってません
参考
はてブロの記事が投稿から1年以上経過している場合にアラートを表示する
経緯
ありがたいことに投稿日から数年経過しても、検索からアクセスされてる記事がいくつかあるんだけど、情報が古いから○iitaみたいに この記事は公開日からn年以上が経過しています。情報が古い可能性がありますので十分ご注意ください。
っていう注意書きを画面上に表示させたかった
注意
- ブログデザインのテーマははてブロ公式のEpicを使用しています。本記事で紹介するコードはあくまで左記テーマに限ったものになります。
- カスタマイズで編集するCSSやJSのコードについては利用上の注意をご確認の上、編集ください。 詳しくはヘルプをご確認ください。
カスタマイズ
1. CSSを追加
管理画面からデザイン
>カスタマイズ
画面を開く
デザインCSS
にalert用のスタイルを追加 (Bootstrapのwarning alertと同じ)
.warning-alert { font-size: 12px; background-color: #ebd1b6; padding: 0.75rem 1.25rem; color: #856404; background-color: #fff3cd; border: 1px solid #fae39f; border-radius: 0.25rem; }
2. JavaScriptの追加
設定
>詳細設定
画面を開く
<head>要素にメタデータを追加
に以下のJSを追加する
document.addEventListener("DOMContentLoaded", function () { const dtElements = document.querySelectorAll(".date.entry-date.first time"); if (dtElements.length === 0) { return; } const nowTs = new Date().getTime(); dtElements.forEach((element) => { if (!element) { return; } const dt = element.getAttribute("datetime"); const postedTs = new Date(dt).getTime(); const diffYears = ((nowTs - postedTs) / (1000 * 60 * 60 * 24) / 365).toFixed(); // 記事公開日から1年以上経過している場合にalertを表示する if (diffYears >= 1) { const div = document.createElement("div"); div.innerText = `この記事は公開日から${diffYears}年以上が経過しています。情報が古い可能性がありますので十分ご注意ください。`; div.classList.add("warning-alert"); element.closest(".entry-header").prepend(div); } }); });
上記の設定が完了後に記事一覧・詳細画面で以下のように表示されます:)
MySQLのDELETE文をEXPLAINで確認する
MySQL5.5でDELETE文をEXPLAINで見ようとしたらエラーになった
mysql> EXPLAIN DELETE FROM samples WHERE title = "title"; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DELETE FROM samples WHERE title = "title"' at line 1
EXPLAINでDELETE文が使えるのは5.6.3からで5.5系では実行できないみたい(泣)
MySQL 5.6.3 現在、EXPLAIN に使用できる説明可能なステートメントは、SELECT、DELETE、INSERT、REPLACE、および UPDATE です。MySQL 5.6.3 より前では、SELECT が唯一の説明可能なステートメントです。
5.5で確認したいときは代替として以下で対応する
EXPLAIN SELECT 1 FROM samples WHERE title = "title";
MySQLで連番のカラムを作成する
いつ使うの?
- AUTO_INCREMENTのidとは別に仮の連番をMySQLで生成したいとき
前提
↓みたいなテーブルにMySQLで生成した連番のidを追加する
id | category |
---|---|
4291 | アニメ |
11 | 映画 |
9302 | 漫画 |
322 | ドラマ |
SQL
SELECT category, @tmp_id := @tmp_id + 1 tmp_id FROM ( SELECT @tmp_id := 0 FROM DUAL ) tmp, samples
結果
id | category | tmp_id |
---|---|---|
4291 | アニメ | 1 |
11 | 映画 | 2 |
9302 | 漫画 | 3 |
322 | ドラマ | 4 |
こんな処理を使わなきゃいけない場合はだいたい設計がおかしいから見直そうな!
コンストラクタで非同期処理を実施しているクラスのテストをJestで書く
constructor()で非同期処理を実施しているクラスのテストを書きたかったときの備忘録
↓みたいにconstructor()で非同期処理を実施していて、Promiseをreturnしていない
export default class A { private readonly list: any; constructor() { new Repository().fetch().then((res) => (this.list = res)); } public b() { return this.list; } }
テストしたい部分をasync()で囲ってあげればOK
test("sample", () => { async () => { expect(new A().b()).toBe([]); }; });
$ npm run test > xxxx@1.0.0 test > jest PASS __tests__/test.test.js ✓ sample (1 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.056 s Ran all test suites.
自信ないんだけど合ってる...? 間違ってたり、他に方法があったらご指摘ください!
TypeScriptでJestを使う
必要なパッケージのinstall
$ npm install --save-dev @babel/preset-typescript
babel.config.js
に追記
module.exports = { presets: [ ["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript" // 追加 ], };
テストの実施
$ npm run test > xxxxxx@1.0.0 test > jest PASS __tests__/sample.test.js ✓ jest sample (18 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.665 s, estimated 1 s
テストが通ることを確認できればOK
全部公式に書いてあるからちゃんと読もうな
*1:必要なパッケージにts-jestを含めていましたがbabelを使用している場合は不要のため修正しました。
babel+jestで型チェックも行いたい場合は必要になります。id:munieru_jp様コメントありがとうございました!
Slackの投稿を1ヶ月単位でスプレッドシートに書き込む【GoogleAppsScript】
現状・やりたいこと
- プライベートで使ってるSlackがフリープラン
- 過去の投稿を残しておきたい
- 保存する投稿はDMのやりとり
やること
1. Slackでユーザーbotの作成
試してないけどアプリbotだとできなさそうだからユーザーbotにした
(てかDMにアプリbotって追加できないよね。。。?)
メッセージ取得に必要な権限を設定してユーザーbotの作成
2. SlackAPIでSlackに投稿した1ヶ月分の投稿の取得
エンドポイントは https://api.slack.com/api/conversations.history
最初channel
にDM相手のユーザーIDを指定しててずっとchannel_not_found
が返ってきてハマった。。。
↓DMにもチャンネルIDあるの知らんかった
メッセージ取得件数がデフォルトで100件ほどだからresponse_metadata.next_cursor
が空になるまでfetchMessages()
を実行する
function fetchMessages(results = [], cursor = "") { const res = UrlFetchApp.fetch( "https://api.slack.com/api/conversations.history?channel=" + CHANNEL_NAME + "&oldest=" + oldest + "&latest=" + latest + "&cursor=" + cursor, { method: "get", payload: { token: USER_TOKEN, }, } ); const src = JSON.parse(res); if (!src.ok) { Logger.log(src); throw new Error("レスポンス不正"); } results.push(...src.messages); // response_metadata.next_cursorがある場合は再帰処理 if (src?.response_metadata?.next_cursor) { return fetchMessages(results, src.response_metadata.next_cursor); } return results; }
3. スプレッドシートにシートの作成と書き込み
function myFunction() { const ss = SpreadsheetApp.openById(SHEET_ID); const sheet = ss.insertSheet(); const dt = new Date(); // 前月分の投稿を取得するため-1ヶ月にする dt.setMonth(dt.getMonth() + 1 - 2) const dateArray = dt.toLocaleDateString().split("/"); // toLocaleDateString()のformatがMM/DD/YYYYになるのはなんで...?? // 年月毎にシートの作成 sheet.setName(dateArray[2] + "-" + (dateArray[0].length === 1 ? "0" + dateArray[0] : dateArray[0])); const src = fetchMessages(); // 日付の昇順で登録したいからsort src.sort((a, b) => a.ts - b.ts); const results = []; src.map((row) => { const dt = new Date(row.ts * 1000).toLocaleString("JP"); results.push([dt, USERS[row.user], row.text]); // 時間・ユーザー名・メッセージ }); sheet.getRange(1, 1, results.length, 3).setValues(results); // 一括書き込み }
4. GASのトリガー設定
トリガーの設定を毎月1日に設定する
あとはスプレッドシートに書き込まれればOK
コード全文
感想
UrlFetchApp.fetch
でparams
指定できんの知らんかった
文字列連結むりすぎ
参考
api.slack.com developers.google.com
株式会社エイルシステムではWebエンジニア・モバイルアプリエンジニアを募集しています。
実務経験がなくてもOKです。ご興味のある方は弊社HPよりご連絡ください。