From 6523affaad111973faf5b790300152a7d4527760 Mon Sep 17 00:00:00 2001 From: ci7lus <7887955+ci7lus@users.noreply.github.com> Date: Mon, 10 Jan 2022 02:18:15 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20miraktest-twitter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + package.json | 4 + src/miraktest-twitter/README.md | 12 + src/miraktest-twitter/components/Tweet.tsx | 431 ++++++++++++++++++ .../components/TwitterRenderer.tsx | 234 ++++++++++ src/miraktest-twitter/constants.ts | 10 + src/miraktest-twitter/index.tsx | 54 +++ src/miraktest-twitter/types.ts | 36 ++ src/miraktest-twitter/utils.ts | 10 + webpack.config.ts | 4 + yarn.lock | 67 ++- 11 files changed, 859 insertions(+), 5 deletions(-) create mode 100644 src/miraktest-twitter/README.md create mode 100644 src/miraktest-twitter/components/Tweet.tsx create mode 100644 src/miraktest-twitter/components/TwitterRenderer.tsx create mode 100644 src/miraktest-twitter/constants.ts create mode 100644 src/miraktest-twitter/index.tsx create mode 100644 src/miraktest-twitter/types.ts create mode 100644 src/miraktest-twitter/utils.ts diff --git a/README.md b/README.md index bf2e00b..e28d999 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ ニコニコ実況(過去ログ API)から視聴中の録画番組のコメントを取得します。コメントの表示には Zenza か DPlayer プラグインが必要です。 - [Gyazo](./src/miraktest-gyazo) スクリーンショットを Gyazo にアップロードします。 +- [Twitter](./src/miraktest-twitter) + 視聴中の番組に関連するツイートを投稿します。 ## ビルド diff --git a/package.json b/package.json index 0c72cc1..1d2f832 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/postcss-import": "^12.0.1", "@types/react-table": "^7.7.5", "@types/tailwindcss": "^2.2.1", + "@types/twitter-text": "^3.1.1", "@types/url-join": "^4.0.1", "@types/webpack": "^4", "@types/webpack-bundle-analyzer": "^4.4.1", @@ -50,6 +51,7 @@ "sass-loader": "^10", "tailwindcss": "^2.2.16", "ts-node": "^10.2.1", + "twitter-d": "^0.4.0", "typescript": "^4.4.3", "webpack": "^4", "webpack-bundle-analyzer": "^4.5.0", @@ -73,6 +75,8 @@ "react-use": "^17.3.1", "recoil": "^0.4.1", "reconnecting-websocket": "^4.4.0", + "twitter-api-client": "^1.5.1", + "twitter-text": "^3.1.0", "url-join": "^4.0.1", "yaml": "^1.10.2" }, diff --git a/src/miraktest-twitter/README.md b/src/miraktest-twitter/README.md new file mode 100644 index 0000000..effb2a1 --- /dev/null +++ b/src/miraktest-twitter/README.md @@ -0,0 +1,12 @@ +# miraktest-twitter + +視聴中の番組に関連するツイートを投稿するためのプラグインです。 +Twitter アカウントの認証情報が必要です。などで取得可能です。 + +## 動作イメージ + +[![Image from Gyazo](https://i.gyazo.com/35e5765188791dc4f9ccbf5ee3de1805.png)](https://gyazo.com/35e5765188791dc4f9ccbf5ee3de1805) + +## ダウンロード + + diff --git a/src/miraktest-twitter/components/Tweet.tsx b/src/miraktest-twitter/components/Tweet.tsx new file mode 100644 index 0000000..befdac7 --- /dev/null +++ b/src/miraktest-twitter/components/Tweet.tsx @@ -0,0 +1,431 @@ +import clsx from "clsx" +import React, { useEffect, useRef, useState } from "react" +import { RotateCw } from "react-feather" +import { + AccountVerifyCredentials, + Search, + TwitterClient, +} from "twitter-api-client" +import type { Status } from "twitter-d" +import twitterText from "twitter-text" +import { ContentPlayerPlayingContent } from "../../@types/plugin" +import { SayaDefinition, TwitterSetting } from "../types" +import { blobToBase64Uri } from "../utils" + +export const TweetComponent: React.FC<{ + setting: Required> + playingContent: ContentPlayerPlayingContent | null + sayaDefinition: SayaDefinition + imageUrl: string | null +}> = ({ setting, playingContent, sayaDefinition, imageUrl }) => { + const [text, setText] = useState("") + const [hashtag, setHashtag] = useState("") + const [tweetText, setTweetText] = useState("") + const [remaining, setRemaining] = useState(280) + const [isValid, setIsValid] = useState(true) + useEffect(() => { + const tweetText = [text, hashtag].join(" ").trim() + setTweetText(tweetText) + const { valid, weightedLength } = twitterText.parseTweet(tweetText) + setIsValid(valid) + setRemaining(280 - weightedLength) + }, [text, hashtag]) + const [isPosting, setIsPosting] = useState(false) + const [images, setImages] = useState([]) + const [selectedImages, setSelectedImages] = useState([]) + const [failed, setFailed] = useState("") + const [serviceTags, setServiceTags] = useState([]) + const [suggestedTags, setSuggestedTags] = useState([]) + const [suggestObtaining, setSuggestObtaining] = useState(false) + const [user, setUser] = useState(null) + + const twitter = new TwitterClient({ + apiKey: setting.consumerKey, + apiSecret: setting.consumerSecret, + accessToken: setting.accessToken, + accessTokenSecret: setting.accessTokenSecret, + }) + + useEffect(() => { + twitter.accountsAndUsers + .accountVerifyCredentials() + .then( + ( + user: AccountVerifyCredentials | { data: AccountVerifyCredentials } + ) => { + setUser("data" in user ? user.data : user) + } + ) + .catch(console.error) + }, [setting]) + + useEffect(() => { + const serviceId = playingContent?.service?.serviceId + if (!serviceId) { + setServiceTags([]) + return + } + const twitterKeywords = sayaDefinition.channels.find((channel) => + channel.serviceIds.includes(serviceId) + )?.twitterKeywords + if (!twitterKeywords) { + setServiceTags([]) + return + } + setServiceTags(twitterKeywords) + }, [playingContent]) + + useEffect(() => { + if (!imageUrl) { + return + } + setImages((images) => [imageUrl, ...images.slice(0, 30)]) + }, [imageUrl]) + const formRef = useRef(null) + + return ( + + + ツイート + {failed && ( + {failed} + )} + + + + + { + e.currentTarget.scrollLeft += e.deltaY + }} + > + {[...serviceTags, ...suggestedTags].map((tag) => ( + { + setHashtag((prev) => `${prev} ${tag}`.trim()) + }} + disabled={(hashtag + " ").includes(tag + " ")} + > + {tag} + + ))} + + { + if (!serviceTags || serviceTags.length === 0) { + return + } + setFailed("") + setSuggestObtaining(true) + twitter.tweets + .search({ + q: serviceTags.join(" OR ") + " exclude:retweets", + locale: "ja", + }) + .then((tweets: Search | { data: Search }) => { + const statuses = + "data" in tweets ? tweets.data.statuses : tweets.statuses + console.debug(statuses) + setSuggestedTags( + Array.from( + new Set( + statuses + .filter( + (status) => status.entities.hashtags.length <= 5 + ) + .map( + (status) => + ( + status.entities + .hashtags as Status["entities"]["hashtags"] + )?.map((hashtag) => "#" + hashtag.text) ?? [] + ) + .flat() + ) + ).filter((hashtag) => !serviceTags.includes(hashtag)) + ) + }) + .catch((e) => { + console.error(e) + setFailed("タグの取得に失敗しました") + }) + .finally(() => { + setSuggestObtaining(false) + }) + }} + > + + + + + {user ? ( + + + @{user.screen_name} + + ) : ( + 認証中... + )} + { + e.preventDefault() + setIsPosting(true) + setFailed("") + Promise.all( + selectedImages.map(async (image) => { + const response = await fetch(image) + const uri = await blobToBase64Uri(await response.blob()) + const uploadResult = await twitter.media.mediaUpload({ + media: uri.split(",")[1], + }) + return uploadResult.media_id_string + }) + ) + .then((mediaIds) => { + twitter.tweets + .statusesUpdate({ + status: tweetText, + media_ids: mediaIds.join(",") || undefined, + }) + .then(() => { + setText("") + setSelectedImages([]) + }) + .catch((e) => { + console.error(e) + if ("data" in e) { + const data = JSON.parse(e.data) + setFailed( + "ツイートに失敗しました: " + data.message || e.data + ) + } else { + setFailed("ツイートに失敗しました") + } + }) + .finally(() => { + setIsPosting(false) + }) + }) + .catch((e) => { + console.error(e) + if ("data" in e) { + const data = JSON.parse(e.data) + setFailed( + "画像のアップロードに失敗しました: " + data.error || + e.data + ) + } else { + setFailed("画像のアップロードに失敗しました") + } + setIsPosting(false) + }) + }} + > + + setHashtag(e.target.value)} + disabled={isPosting} + /> + + + setText(e.target.value)} + required={selectedImages.length === 0} + onKeyDown={(e) => { + if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { + formRef.current?.requestSubmit() + } + }} + disabled={isPosting} + > + + + + {remaining.toLocaleString()} + + + + setSelectedImages([])} + > + 画像の選択を削除 + + + ツイート + + + + + + + {images.map((imageUrl) => ( + { + setSelectedImages((selectedImages) => + selectedImages.includes(imageUrl) + ? selectedImages.filter((image) => image !== imageUrl) + : 4 <= selectedImages.length + ? selectedImages + : [...selectedImages, imageUrl] + ) + }} + key={imageUrl} + className={clsx("relative")} + > + + + {selectedImages.includes(imageUrl) + ? selectedImages.indexOf(imageUrl) + 1 + : ""} + + + ))} + + + + + ) +} diff --git a/src/miraktest-twitter/components/TwitterRenderer.tsx b/src/miraktest-twitter/components/TwitterRenderer.tsx new file mode 100644 index 0000000..d7d80b2 --- /dev/null +++ b/src/miraktest-twitter/components/TwitterRenderer.tsx @@ -0,0 +1,234 @@ +import Axios from "axios" +import React, { useEffect, useState } from "react" +import { atom, useRecoilValue, useRecoilState, useSetRecoilState } from "recoil" +import YAML from "yaml" +import { InitPlugin } from "../../@types/plugin" +import tailwind from "../../tailwind.scss" +import { + TWITTER_META, + TWITTER_PLUGIN_PREFIX, + TWITTER_TWEET_WINDOW_ID, +} from "../constants" +import { TwitterSetting, SayaDefinition } from "../types" +import { TweetComponent } from "./Tweet" + +export const TwitterRenderer: InitPlugin["renderer"] = ({ + appInfo, + rpc, + atoms, +}) => { + const settingAtom = atom({ + key: `${TWITTER_PLUGIN_PREFIX}.setting`, + default: { isContentInfoEmbedInImageEnabled: false }, + }) + const imageUrlAtom = atom({ + key: `${TWITTER_PLUGIN_PREFIX}.imageUrl`, + default: null, + }) + + return { + ...TWITTER_META, + exposedAtoms: [], + sharedAtoms: [ + { + type: "atom", + atom: settingAtom, + }, + { + type: "atom", + atom: imageUrlAtom, + }, + ], + storedAtoms: [ + { + type: "atom", + atom: settingAtom, + }, + ], + setup() { + return + }, + components: [ + { + id: `${TWITTER_PLUGIN_PREFIX}.settings`, + position: "onSetting", + label: TWITTER_META.name, + component: () => { + const [setting, setSetting] = useRecoilState(settingAtom) + const [consumerKey, setConsumerKey] = useState(setting.consumerKey) + const [consumerSecret, setConsumerSecret] = useState( + setting.consumerSecret + ) + const [accessToken, setAccessToken] = useState(setting.accessToken) + const [accessTokenSecret, setAccessTokenSecret] = useState( + setting.accessTokenSecret + ) + const [ + isContentInfoEmbedInImageEnabled, + // TODO: setIsContentInfoEmbedInImageEnabled, + ] = useState(setting.isContentInfoEmbedInImageEnabled) + return ( + <> + + { + e.preventDefault() + setSetting({ + consumerKey: consumerKey || undefined, + consumerSecret: consumerSecret || undefined, + accessToken: accessToken || undefined, + accessTokenSecret: accessTokenSecret || undefined, + isContentInfoEmbedInImageEnabled: + isContentInfoEmbedInImageEnabled, + }) + }} + > + + Twitter 認証情報 + setConsumerKey(e.target.value)} + /> + setConsumerSecret(e.target.value)} + /> + setAccessToken(e.target.value)} + /> + setAccessTokenSecret(e.target.value)} + /> + {/* + 画像に番組情報を埋め込む + + setIsContentInfoEmbedInImageEnabled( + (enabled) => !enabled + ) + } + /> + */} + + + 保存 + + + > + ) + }, + }, + { + id: `${TWITTER_PLUGIN_PREFIX}.contentplayer`, + position: "onSplash", + component: () => { + const url = useRecoilValue(atoms.contentPlayerScreenshotUrlSelector) + const setImageUrl = useSetRecoilState(imageUrlAtom) + useEffect(() => { + if (!url) { + return + } + setImageUrl(url) + }, [url]) + return <>> + }, + }, + ], + destroy() { + return + }, + windows: { + [TWITTER_TWEET_WINDOW_ID]: () => { + const setting = useRecoilValue(settingAtom) + const activeId = useRecoilValue( + atoms.globalActiveContentPlayerIdSelector + ) + const playingContent = useRecoilValue( + atoms.globalContentPlayerPlayingContentFamily(activeId ?? 0) + ) + const imageUrl = useRecoilValue(imageUrlAtom) + useEffect(() => { + rpc.setWindowTitle(`ツイートする - ${appInfo.name}`) + }, []) + const [sayaDefinition, setSayaDefinition] = + useState(null) + useEffect(() => { + // TODO: LOAD + Axios.get( + "https://raw.githack.com/SlashNephy/saya/dev/docs/definitions.yml", + { + responseType: "text", + } + ) + .then((r) => { + const parsed: SayaDefinition = YAML.parse(r.data) + setSayaDefinition(parsed) + }) + .catch(console.error) + }, []) + + const isCredentialFulfilled = + setting.consumerKey && + setting.consumerSecret && + setting.accessToken && + setting.accessTokenSecret + + return ( + <> + + + {setting.consumerKey && + setting.consumerSecret && + setting.accessToken && + setting.accessTokenSecret && + sayaDefinition ? ( + } + playingContent={playingContent} + sayaDefinition={sayaDefinition} + imageUrl={imageUrl} + /> + ) : ( + + + {isCredentialFulfilled ? ( + 読込中です… + ) : ( + <> + + Twitter の設定が行われていません。 + + 設定から認証情報を設定してください。 + > + )} + + + )} + + > + ) + }, + }, + } +} diff --git a/src/miraktest-twitter/constants.ts b/src/miraktest-twitter/constants.ts new file mode 100644 index 0000000..c4ca6db --- /dev/null +++ b/src/miraktest-twitter/constants.ts @@ -0,0 +1,10 @@ +export const TWITTER_PLUGIN_ID = "io.github.ci7lus.miraktest-plugins.twitter" +export const TWITTER_PLUGIN_PREFIX = "plugins.ci7lus.twitter" +export const TWITTER_META = { + id: TWITTER_PLUGIN_ID, + name: "Twitter", + author: "ci7lus", + version: "0.0.1", + description: "視聴中の番組に関連するツイートを投稿する", +} +export const TWITTER_TWEET_WINDOW_ID = `${TWITTER_PLUGIN_ID}.tweet` diff --git a/src/miraktest-twitter/index.tsx b/src/miraktest-twitter/index.tsx new file mode 100644 index 0000000..e59ca2f --- /dev/null +++ b/src/miraktest-twitter/index.tsx @@ -0,0 +1,54 @@ +import { InitPlugin } from "../@types/plugin" +import { TWITTER_META, TWITTER_TWEET_WINDOW_ID } from "./constants" + +/** + * MirakTest Twitter Plugin + * 視聴中の番組に関連するツイートを投稿するプラグイン + */ + +const main: InitPlugin = { + renderer: + typeof window !== "undefined" + ? // eslint-disable-next-line @typescript-eslint/no-var-requires + require("./components/TwitterRenderer").TwitterRenderer + : undefined, + main: ({ functions }) => { + return { + ...TWITTER_META, + setup: () => { + return + }, + destroy: () => { + return + }, + appMenu: { + label: "ツイート画面を開く", + click: () => { + functions.openWindow({ + name: TWITTER_TWEET_WINDOW_ID, + isSingletone: true, + args: { + width: 600, + height: 400, + }, + }) + }, + }, + contextMenu: { + label: "ツイート画面を開く", + click: () => { + functions.openWindow({ + name: TWITTER_TWEET_WINDOW_ID, + isSingletone: true, + args: { + width: 600, + height: 400, + }, + }) + }, + }, + } + }, +} + +export default main diff --git a/src/miraktest-twitter/types.ts b/src/miraktest-twitter/types.ts new file mode 100644 index 0000000..4952925 --- /dev/null +++ b/src/miraktest-twitter/types.ts @@ -0,0 +1,36 @@ +export type TwitterSetting = { + consumerKey?: string + consumerSecret?: string + accessToken?: string + accessTokenSecret?: string + isContentInfoEmbedInImageEnabled: boolean +} + +export type SayaDefinitionBoard = { + id: string + name: string + server: string + board: string +} + +export type SayaDefinitionChannel = { + annictId?: number + boardIds: string[] + flag: number + hasOfficialNicolive: boolean + miyoutvId?: string + name: string + networkId: number + nicojkId?: number + nicoliveCommunityIds: string[] + nicoliveTags: string[] + serviceIds: number[] + syobocalId?: number + twitterKeywords: string[] + type: "GR" | "BS" | "CS" +} + +export type SayaDefinition = { + boards: SayaDefinitionBoard[] + channels: SayaDefinitionChannel[] +} diff --git a/src/miraktest-twitter/utils.ts b/src/miraktest-twitter/utils.ts new file mode 100644 index 0000000..fb5806e --- /dev/null +++ b/src/miraktest-twitter/utils.ts @@ -0,0 +1,10 @@ +export const blobToBase64Uri = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + resolve(reader.result as never) + } + reader.onerror = reject + reader.readAsDataURL(blob) + }) +} diff --git a/webpack.config.ts b/webpack.config.ts index fba6fa0..4b5a431 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -58,6 +58,10 @@ const entries: Entry[] = [ name: "miraktest-gyazo", dir: "./src/miraktest-gyazo", }, + { + name: "miraktest-twitter", + dir: "./src/miraktest-twitter", + }, ] const config: ( diff --git a/yarn.lock b/yarn.lock index 9f2f8c6..ae596e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -440,6 +440,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.1": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194" @@ -1077,9 +1084,9 @@ integrity sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg== "@types/node@^16.10.2": - version "16.10.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e" - integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ== + version "16.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.17.tgz#ae146499772e33fc6382e1880bc567e41a528586" + integrity sha512-C1vTZME8cFo8uxY2ui41xcynEotVkczIVI5AjLmy5pkpBv/FtG+jhtOlfcPysI8VRVwoOMv6NJm44LGnoMSWkw== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1150,6 +1157,11 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== +"@types/twitter-text@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/twitter-text/-/twitter-text-3.1.1.tgz#c23334522ca252659e432635eec2f439706737d0" + integrity sha512-TqUdwZOdCi3pB4kmCyOS6sjLfzRjbqcwFuVKGQ9Gq9J37GBnF7mf6cIw/zN2GgDbvXYEEfoiuPGFw+TK5ZU0EA== + "@types/uglify-js@*": version "3.13.1" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea" @@ -2182,7 +2194,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.7.0: +buffer@^5.6.0, buffer@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -2677,6 +2689,11 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" +core-js@^2.5.0: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -5685,6 +5702,11 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +oauth@^0.9.15: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= + object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5714,6 +5736,13 @@ object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object-sizeof@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/object-sizeof/-/object-sizeof-1.6.1.tgz#35971f3fd2102bd8b51c67b0a53ed773ff77ab56" + integrity sha512-gNKGcRnDRXwEpAdwUY3Ef+aVZIrcQVXozSaVzHz6Pv4JxysH8vf5F+nIgsqW5T/YNwZNveh0mIW7PEH1O2MrDw== + dependencies: + buffer "^5.6.0" + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -6322,7 +6351,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4: +punycode@1.4.1, punycode@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -7743,6 +7772,11 @@ twemoji-parser@13.1.0: resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4" integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg== +twemoji-parser@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-11.0.2.tgz#24e87c2008abe8544c962f193b88b331de32b446" + integrity sha512-5kO2XCcpAql6zjdLwRwJjYvAZyDy3+Uj7v1ipBzLthQmDL7Ce19bEqHr3ImSNeoSW2OA8u02XmARbXHaNO8GhA== + twemoji@^13.0.1: version "13.1.0" resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-13.1.0.tgz#65bb71e966dae56f0d42c30176f04cbdae109913" @@ -7753,6 +7787,29 @@ twemoji@^13.0.1: twemoji-parser "13.1.0" universalify "^0.1.2" +twitter-api-client@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/twitter-api-client/-/twitter-api-client-1.5.1.tgz#9823ce8370fd6001934a6be0d14e0062cd549c43" + integrity sha512-I+hdaoFdkV/ilFRqHGgi5NrTiY6unS/75N9pelN3qBQ2rQh9s0TwcdFkjUOCRQoHkXcEUI4AH2XULN01XvBMMA== + dependencies: + oauth "^0.9.15" + object-sizeof "^1.6.1" + +twitter-d@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/twitter-d/-/twitter-d-0.4.0.tgz#3b7e2dd480c38699f4199124b77511c6f7bc8e29" + integrity sha512-t8z7qqq0yt1dD7BUISRvASw9mtYRMSTTVzmkvjz8oM9ZN++LEC6Ix8WEXZS0RtswnGgwdSPTxY9qyHcYgPexdA== + +twitter-text@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/twitter-text/-/twitter-text-3.1.0.tgz#798e932b289f506efe2a1f03fe917ba30627f125" + integrity sha512-nulfUi3FN6z0LUjYipJid+eiwXvOLb8Ass7Jy/6zsXmZK3URte043m8fL3FyDzrK+WLpyqhHuR/TcARTN/iuGQ== + dependencies: + "@babel/runtime" "^7.3.1" + core-js "^2.5.0" + punycode "1.4.1" + twemoji-parser "^11.0.2" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
{failed}
@{user.screen_name}
+ {remaining.toLocaleString()} +
+ {selectedImages.includes(imageUrl) + ? selectedImages.indexOf(imageUrl) + 1 + : ""} +
設定から認証情報を設定してください。