ブラウザ拡張Twishare to Misskeyの開発経緯と実装

# はじめに
俺受験生ですし、本当はアドベントカレンダー書くつもり無かったんですけど、なんか書きたくなったので書きます（大丈夫か）

昨年はクソアプリアドベントカレンダーに1個、書いたんですけど、今年はまともなもん作っちゃったんですよ。それで、今年1年の開発のまとめというか、そんな感じで書いてみようかな、と。

本当は[Misskeyのアドベントカレンダー](https://adventar.org/calendars/8742)やりたかったんですけど、まあ思い立つのが遅すぎて埋まってましたよねはい。

https://adventar.org/calendars/8742

名乗り遅れました、[あるかっぱ/アルパカ本家](https://alpaca-honke.github.io) と申します。各種ウェブサイトをMisskey（Mastodonと同様Activity Pubを利用した分散型SNS）にシェアするためのブラウザ拡張機能、[Twishare to Misskey](https://alpaca-honke.github.io/twishare-to-misskey) を開発しております。インストールや使い方などはこちらのリンク先に記載しておりますのでとりあえず使いたいのーーー！！って人はご覧ください。

https://alpaca-honke.github.io/twishare-to-misskey

https://alpaca-honke.github.io

この記事では開発動機から、動作の仕組み、とかなんか色々書いていきたいと思います。書いたあと読み返しとか軽くしかしない人なので[^1]ミスってたらすいません
[^1]: 時間ないってのもあるけど基本面倒くさがりなので

れっつごー

# 動機
僕自身、Misskeyに登録したのはTwitterAPI大騒動（2023.3）頃だったと思います。わざわざプロフィール見に行くのめんどくさいので正確な日付はわからん[^2]
[^2]: いや確認しろよ

https://misskey.io/@alpaca_honke

Misskeyのアカウントを作成して1ヶ月ほどたった2023年4月、WebページをMisskeyにシェアする手段がほぼないことに気づきました。
ただ、（他の開発者の方々を批判する意図はありませんが）他のブラウザ拡張はアクセストークンを平文で保存しているものが多かったため、ちょっとセキュリティ的に心配かもしれないな、とは思っていました。  
そこで、それ以前に[Misskey Hub](https://misskey-hub.net)[^3]
を見ていたところ、共有フォームという機能があったのを思い出したんです。
[^3]: 多くのインスタンスがあるソフトウェアなので、開発チームが各サーバーとは独立で管理しているMisskeyの公式サイト

共有フォームとは、`https://(Misskeyインスタンスドメイン)/share`にクエリパラメータで`text`や`title`、`url`などを渡すことで外部からユーザーに投稿を促すことができるようなページです。Twitterで言うところの`https://twitter.com/intent/tweet`ですね。  

https://github.com/misskey-dev/misskey/blob/develop/packages/frontend/src/pages/share.vue

現在は主に、未来情報産業株式会社さんの[Misskey Share](https://misskeyshare.link) （診断メーカーに実装されたことで話題になったヤツ）で利用されています。

また、多くのウェブサイトにはTwitterへのシェアボタンが実装されているのでそれを活用できないかと考えました。

僕自身、ストアには公開しませんでしたが、以前ちょっとだけブラウザ拡張作ったことがあったので作ってみようかなぁと。

# 制作の歴史（？）と実装について
## v0.0.x系統
現在はTwishare to Misskeyとして、/shareが存在する全てのサーバーへのシェアを提供していますが、もともとはTwishare to ioという、Misskey.ioのみにシェアするものでした。  
Twishare to ioは、UIはほぼなく、単にtwitterシェアボタンをジャックするだけでした。

具体的には、拡張機能のmanifest.jsonにおいて、https://twitter.com/intent/tweet のページが開かれた時にロード後確認画面を出して、URLに渡されたパラメータを多少整形。それから https://misskey.io/share に渡す、って感じです。

https://github.com/alpaca-honke/twishare-to-misskey/commit/71471f9a9a8e65ee10119d5a7b1439d1d17743b0#diff-ed3ee7e0beea2498ff3b8ca85973d122fc6fa3d585d62b5807ec034d0cf076b3

これを公開したところ、なんかめちゃくちゃ偉業[^4]いただいて、なんか、めちゃくちゃテンション爆アゲで。  
（ちなみに、Chromeウェブストアのデベロッパー登録時に$5をGoogleに納めなきゃ行けないんですが、当時の相場で620円くらいだったのになんか端数切り上げられて700円取られました。金欠男子高校生の80円を返してくれGoogle）
[^4]: リアクション絵文字の1種。

## v0.1.x系統
通知破壊[^5]を眺めていたら、現在は僕が主に生息しているサーバーである[もこきー](https://mkkey.net) の管理人である、たこさん[@emtk@mkkey.net](https://mkkey.net/@emtk) （当時は面識なかった）が、もこきー向けにもシェアできるようにフォークしてくれていたんですね。  
そこで他鯖へのシェアできるようにする改造ありがとう他のみんなもこんな感じでやってね的な意味で、「他鯖への対応大歓迎です」って引用したら、（確実にこれは言葉足らずな俺が悪いんだけれども[^6]、）io以外のサーバにもシェアできるように実装していただいたんです！  
しかもちゃんとシェア先のサーバーをストレージに保存するという。最高ですね。  
（実はこの話まだたこさんにしてない。その節はなんかこっちからお願いしたような形になっちゃってすいませんでした、そして嫌な顔せず速攻PRだしてくれてありがとうございます）
[^5]: 大量のリアクションやリノート（リツイート的な）によって通知が荒れること
[^6]: マジでこいつ日本語大丈夫か？

https://github.com/alpaca-honke/twishare-to-misskey/pull/1

当時のスクショ  

1. たこさんによる実装  
![1000004340.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819245/71bc5147-aec8-22a9-d024-65604fba6136.png)

2. デザイン調整後  
![すくしょ](https://raw.githubusercontent.com/alpaca-honke/twishare-to-misskey/0e73d70ef3b0802b340c4ada8fc6430e80dc32a1/images/option_ui.png)

## v0.2.0系統
その後、5月頃ですかね、Twitterシェアボタンが存在しないページでも共有できるように、画面右下に拡張機能付属のシェアボタンを設置しました。これは今ではTwishareの象徴（だと勝手に思ってる）デザインです[^7]。ここがv0.2.0ですね。
[^7]: ポップアップの設定画面の方が象徴的とする説もある。開発者もようわかっとらん

こいつ  
![すくしょ](https://raw.githubusercontent.com/alpaca-honke/twishare-to-misskey/2c4d674732b3e06ae327da545bcf39f602d9234d/images/floationg.png)

まあこれも簡単な構造で、現在のページのタイトルとURLを取得して/shareに投げてるだけです。

それから、ボタンの表示非表示に関する設定とか諸々を追加したりはしましたがまあ特筆することは、ないかなぁ。

あえていえば、現在表示中のページがMisskeyまたはFirefish(旧Calckey)であるかの判別方法は書いておきましょうか。  
これは、MisskeyのページをMisskeyにシェアしたとて仕方ないのでMisskey上では非表示にするオプションに使用してます。

`meta`タグの`name`属性を元に判断してますね。

```javascript
async function isMisskey() {
        // MisskeyやCalckeyでは、metaタグのname='application-name'にcontent='Misskey'とか'Calckey'がついてる
        let metatags = document.getElementsByTagName('meta');
    const misskeys = ['Misskey','Calckey','Firefish'];
        for (const metatag of metatags){
                if (
                        metatag.getAttribute('name') === 'application-name' &&
            misskeys.includes(metatag.getAttribute('content'))
                ){
                        return true;
                }
        }
        return false;
}
```

それから、ポップアップのデザイン改善を行いました[^8]。
[^8]: 初期のデザインが壊滅的すぎた

こんな感じ  
![すくしょ](https://raw.githubusercontent.com/alpaca-honke/twishare-to-misskey/91d56a649cb5805c1c4fc9cb437b16b362afc9ec/images/option_ui.png)

その後、鈴音りんさん（[@rin_jugatla@misskey.io](https://misskey.io/@rin_jugatla) ）にプルリクエストをいただき、YouTubeとかで全画面表示にしてる時に右下のシェアボタンが隠れるようにしていただきました。  
僕基本的に全画面表示しない人なので、こういうの気づかないんで、めちゃありがたかったです。

それから、Twitterにおけるツイート詳細画面をシェアする際に、ツイート内容をMFM[^9]の引用型としてノートに貼り付ける機能を実装しました。v0.2.5です。これは自分でもよくやったと思う。元々Misskeyには、ツイートのURLを貼るとツイートを埋め込んでくれる機能はあるんですけど、これをすることでツイートを展開しなくても内容はわかるようになった。
[^9]: Misskeyの各所で使用できる、文字列装飾用の書式。もともとはMarkdownの派生だったらしいが、MFM関数の充実で色んな装飾ができるようになっている。参考: [Misskey Hub](https://misskey-hub.net/docs/features/mfm.html)

この際、ツイート内でTwitterユーザー名に対してメンションが行われていた時にMisskey側でメンションされないように、`<plain>`で囲む工夫を行いました。（無駄に1行で書いてるね。なんでだろうね。分けた方が読みやすいのにね[^10]。）
[^10]: こいつアホかな。

```JavaScript
const replacedTweetText = tweetText.split('\n').map(line => line ? '><plain>' + line + '</plain>' : '>').join('\n');
（中略）
let shareText = `${replacedTweetText}\n>by <plain>${tweetUsername}</plain>\n\n${urlToShare.href}`;
```

ほぼ同時に、ツイート内に絵文字が含まれていると、（Twemojiは画像扱いされるので）Misskey側には表示されなかったため、TwemojiをUnicode絵文字に置換するように変更をも行いました。  
単にTwemojiの`image`タグの`alt`属性と置き換えてるだけです。

```javascript
const tweet = document.querySelector('article div[data-testid="tweetText"]');
//TwemojiのUnicode絵文字化
const twemojis = tweet.querySelectorAll('img');
for (const twemoji of twemojis) {
    const emoji = twemoji.alt;
    const emojiTextNode = document.createTextNode(emoji);
    tweet.replaceChild(emojiTextNode, twemoji);
}
const tweetText = tweet.textContent;
```

この辺でv0.2.6出してますね。

## v0.3.x系統とv0.4.x系統
右下のシェアボタンが動かせるようにしてほしいという要望があったので、ドラッグもしくはスワイプで移動できるようにしました。  
要素のドラッグは、ググればいくつか実装例は出てくるんですけど、だいたいそういうのってそのままじゃ上手くいかないんですよね[^11]。
[^11]: コピペで済まそうとすな

Twishareの場合、ドラッグするのがボタンであること、スクロールしても画面の定位置で留まっているべきということなど（他にも問題あった気がするけど忘れた）があり、その上僕の技術力もそこまである訳ではないので、悪戦苦闘しながら実装していました。主に英語の授業中に[^12]。（おい
[^12]: 去年のアドベントカレンダーでも、英語の授業中に書いたとか言ってた気がするこいつ。真面目に授業受けろよ

それで完成したのがこんなコードです。（長いから畳んどきますね。）  
<details>
<summary>ボタンをドラッグさせるコード</summary>

```JavaScript
    //以下クリックorドラッグ時
    //変数の定義
    let isDragging = false;
    let isClickEnabled = true;
    let cursorX;
    let cursorY;

    //挙動を設定:マウスドラッグ
    //ブラウザーデフォルトのドラッグを無効化しておく
    button.ondragstart = () => {
        return false;
    };
    button.addEventListener('mousedown',(event) => {
        if (event.button === 0) {isDragging = true};
    });
    button.addEventListener('mousemove',(event) => {
        if (isDragging && event.button === 0) {
            //ドラッグ
            isClickEnabled = false;
            cursorX = event.clientX;
            cursorY = event.clientY;
            //なんか配置がうまく行かなかったので要素幅の3/4倍がちょうどよかった
            button.style.top = (cursorY-3*button.getBoundingClientRect().height/4) + 'px';
            button.style.left = (cursorX-3*button.getBoundingClientRect().width/4) + 'px';
        } else {
            return;
        };
    });
    button.addEventListener('mouseup',(event) => {
        if (event.button === 0) {
            isDragging = false;
            if (isClickEnabled) buttonClicked();
            isClickEnabled = true;
        };
    });
    //予期せずボタン外でマウスアップされてもドラッグをオフにする
    window.addEventListener('mouseup',() => {
        isDragging = false;
        isClickEnabled = true;
    });

    //挙動を設定:スワイプ
    button.addEventListener('touchstart',(event) => {
        //画面スクロールを止める
        event.preventDefault();
        isDragging = true;
    });
    button.addEventListener('touchmove',(event) => {
        if (isDragging) {
            event.preventDefault();
            //スワイプ
            isClickEnabled = false;
            cursorX = event.changedTouches[0].clientX;
            cursorY = event.changedTouches[0].clientY;
            //なんか配置がうまく行かなかったので要素幅の3/4倍がちょうどよかった
            button.style.top = (cursorY-3*button.getBoundingClientRect().height/4) + 'px';
            button.style.left = (cursorX-3*button.getBoundingClientRect().width/4) + 'px';
        } else {
            return;
        };
    });
    button.addEventListener('touchend',(event) => {
        event.preventDefault();
        isDragging = false;
        if (isClickEnabled) buttonClicked();
        isClickEnabled = true;
    });
    //予期せずボタン外でタッチエンドされてもドラッグをオフにする
    window.addEventListener('touchend',() => {
        isDragging = false;
        isClickEnabled = true;
    });
```

</details>

右下にあるシェアボタンが邪魔で非表示にしている場合でもシェアできるように、拡張機能のポップアップ（サーバーとか設定するやつ）からもシェアできるようにするボタンの設置も行いました。

ただ、これはなんかFirefoxではうまく動かなかった[^13]ので、Chrome向けリリースにしか含まれていません。（これがv0.4.0）
[^13]: FirefoxはChromeよりもセキュリティを重視している傾向があるので、特に拡張機能周りの機能は制限されてることが多い

https://github.com/alpaca-honke/twishare-to-misskey/issues/45

では、Firefox向けにはどうしたかと言うと、前述のボタンドラッグの機能だけを追加した、v0.3.0をリリースしました。

あと、Twishare本体とは別ですが、Twishareの右下のシェアボタンとほぼ同等の動きをするブックマークレットも作成しました。ぜひ使ってね。

https://alpaca-honke.github.io/twishare-to-misskey/bookmarklet.html

ここまでが現在の開発状況です。

なお、冒頭にも書きましたが、受験生なので、今後は少なくとも年度末までは更新できないんじゃないかなぁと思ってます。

# 今後の展望
（現在のユーザー数）  
![Chrome](https://img.shields.io/chrome-web-store/users/fbaifpppndnlbbjcbjdfgbdkoibnipjb?label=users%20%28chrome%20web%20store%29&style=flat-square)  
![Firefox](https://img.shields.io/amo/users/twishare-to-misskey?style=flat-square&label=users%20(mozilla%20add-on))  
↑ありがたいことにこの拡張はおよそ600弱ものユーザーに使用していただいていますので、暫くは更新を続けていきたいと思っています。みんな使ってくれてありがとね[^14]。
[^14]: ほんとに感謝

まずは、Firefoxをv0.4.x系統（ポップアップからもシェアできるようにするやつ）にあげたいかなぁと。

ほかには、ちょくちょく右下のシェアボタンが崩れるので、原因特定と修正をしたいですね。ユーザーの皆さんももし見つけたら気軽に教えてください。

最後に、他の僕のMisskey関連の作品も紹介しておきます。

他の人のノートを自分のプロフィールにピン留めできるプラグイン
https://gist.github.com/alpaca-honke/8b9763e89f5642596ec022926f573e5d

未来情報産業株式会社さんのMisskey Shareを使用してMisskeyにシェアするためのURLを簡単に作れるサイト

https://alpaca-honke.github.io/make-misskeyshare-url

以上、ぱーーって書いたので異常なほど読みづらかったかと思いますけど読んでいただきありがとうございました！  
（いつもはもっと時間かけて書くけど、今回は時間なかったんでごめんなさい）