Skip to content

Commit

Permalink
#6 返信先を選択できるようにした
Browse files Browse the repository at this point in the history
  • Loading branch information
amay077 committed Jan 21, 2025
1 parent d5c1a67 commit bfcd284
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 7 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@atproto/api": "^0.12.7",
"croppie": "^2.6.5",
"dayjs": "^1.11.13",
"vite-plugin-pwa": "^0.20.0"
}
}
75 changes: 71 additions & 4 deletions src/lib/MainContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import MastodonConnection from "./MastodonConnection.svelte";
import BlueskyConnection from "./BlueskyConnection.svelte";
import { loadMessage, loadPostSetting, saveMessage, type SettingType } from "./func";
import TwitterConnection from "./TwitterConnection.svelte";
import { getApiVersion, postSettings, postTo, postToSns } from "./MainContent";
import { getApiVersion, loadMyPosts, postSettings, postTo, postToSns, type Post, type PresentedPost } from "./MainContent";
import ImagePreview from "./ImagePreview.svelte";
import dayjs from "dayjs";
const built_at = (window as any)['built_at'] ?? '';
let apiVer: { build_at: string, env_ver: string } = { build_at: '', env_ver: '' };
let myPosts: PresentedPost[] =[];
let loading = true;
let loadingMyPosts = false;
let posting = false;
let text = loadMessage()?.message ?? '';
Expand All @@ -20,6 +23,15 @@ let expandedReply = false;
let replyToIdForMastodon = '';
let replyToIdForBluesky = '';
let replyToIdForTwitter = '';
let replyToPost: PresentedPost = {
display_posted_at: undefined,
trimmed_text: '',
postOfType: {
mastodon: undefined,
bluesky: undefined,
twitter: undefined,
}
};
onMount(async () => {
console.log(`onMount`);
Expand Down Expand Up @@ -68,9 +80,9 @@ const post = async () => {
};
const res = await postToSns(text, imageDataURLs, { reply_to_ids: {
mastodon: getPostId(replyToIdForMastodon),
twitter: getPostId(replyToIdForTwitter),
bluesky: getPostId(replyToIdForBluesky),
mastodon: getPostId(replyToPost?.postOfType['mastodon']?.url ?? replyToIdForMastodon),
twitter: getPostId(replyToPost?.postOfType['twitter']?.url ?? replyToIdForTwitter),
bluesky: getPostId(replyToPost?.postOfType['bluesky']?.url ?? replyToIdForBluesky),
} });
if (res.errors.length == 0) {
Expand All @@ -80,6 +92,15 @@ const post = async () => {
replyToIdForMastodon = '';
replyToIdForBluesky = '';
replyToIdForTwitter = '';
replyToPost = {
display_posted_at: undefined,
trimmed_text: '',
postOfType: {
mastodon: undefined,
bluesky: undefined,
twitter: undefined,
}
};
alert('投稿しました。');
} else {
alert(`${res.errors.join(', ')}に投稿できませんでした。`);
Expand All @@ -106,6 +127,19 @@ const onVersion = async () => {
apiVer = await getApiVersion();
}
const onLoadMyPosts = async () => {
myPosts = [];
loadingMyPosts = true;
myPosts = await loadMyPosts();
loadingMyPosts = false;
}
const getTypes = (post: PresentedPost) => {
// console.log(`FIXME h_oku 後で消す -> getTypes -> post:`, post);
const types = Object.entries(post.postOfType).filter(([k, v]) => v != null).map(([k, v]) => k);
return types.length > 0 ? `(${types.join(', ')})` : '';
}
</script>

{#if loading}
Expand Down Expand Up @@ -178,6 +212,18 @@ const onVersion = async () => {

<button class="btn btn-primary-outline" on:click="{() => {
text = '';
replyToIdForMastodon = '';
replyToIdForBluesky = '';
replyToIdForTwitter = '';
replyToPost = {
display_posted_at: undefined,
trimmed_text: '',
postOfType: {
mastodon: undefined,
bluesky: undefined,
twitter: undefined,
}
};
onTextChange();
}}" disabled={text.length <= 0}>
Clear
Expand Down Expand Up @@ -208,6 +254,26 @@ const onVersion = async () => {
</div>

{#if expandedReply}

<button class="btn btn-sm btn-primary" on:click={onLoadMyPosts}>
{#if loadingMyPosts}
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
{/if}
Load my posts
</button>
<select class="form-select form-select-sm" bind:value={replyToPost}>
<option>Manual reply</option>
{#each myPosts as post}
<option value={post}>{post.display_posted_at} - {post.trimmed_text} {getTypes(post)}</option>
{/each}
</select>

{#if replyToPost.display_posted_at == undefined}

<div class="my-2"> - OR - </div>

{#if postSettings.mastodon != null && postTo.mastodon}
<div style="width: 100%;" class="d-flex flex-row align-items-center gap-1">
<svg style="width: 18px;" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-mastodon" viewBox="0 0 16 16">
Expand All @@ -234,6 +300,7 @@ const onVersion = async () => {
</div>
{/if}
{/if}
{/if}

</div>

Expand Down
185 changes: 183 additions & 2 deletions src/lib/MainContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import type { ReplyRef } from "@atproto/api/dist/client/types/app/bsky/feed/post
import { Config } from "../config";
import { type SettingDataMastodon, type SettingDataBluesky, type SettingDataTwitter, loadPostSetting, type SettingType, loadMessage, savePostSetting } from "./func";
import { BskyAgent, RichText, type AtpSessionData } from "@atproto/api";
import dayjs from "dayjs";

const bskyEndpoint = 'https://bsky.social';

export type Post = { text: string, url: string, posted_at: Date };
export type PresentedPost = {
display_posted_at: string | undefined,
trimmed_text: string,
postOfType: { [K in SettingType]: Post | undefined },

}

export const postSettings: {
mastodon: SettingDataMastodon | null,
Expand Down Expand Up @@ -31,6 +42,81 @@ export async function getApiVersion(): Promise<{ build_at: string, env_ver: stri
}
}

export const loadMyPosts = async (): Promise<PresentedPost[]> => {

const enableTypes = Array.from(Object.entries(postTo)).filter(([_, v]) => v).map(([k, v]) => (k as SettingType));

const promises = [];

for (const type of enableTypes) {
switch (type) {
case 'mastodon':
promises.push(loadMyPostsMastodon().then(posts => ({ type: 'mastodon', posts })));
break;
case 'bluesky':
promises.push(loadMyPostsBluesky().then(posts => ({ type: 'bluesky', posts })));
break;
case 'twitter':
promises.push(loadMyPostsTwritter().then(posts => ({ type: 'twitter', posts })));
break;
}
}
const posts = await Promise.allSettled(promises);

const succeededPosts = posts.filter((p) => p.status == 'fulfilled').map(x => x.value).reduce((acc, cur) => {

(cur?.posts ?? []).forEach((p) => {
acc.push({ type: cur.type as SettingType, post: p });
});

return acc;
}, [] as { type: SettingType, post: Post }[]);

const trimText = (text: string) => {
const max = 50;
if (text.length > max) {
return text.substring(0, max) + '...';
} else {
return text;
}
}

const groupByText = (input: { type: SettingType, post: Post }[]): PresentedPost[] => {
const grouped: { [key: string]: PresentedPost } = {};

input.forEach(({ type, post }) => {
const key = post.text.substring(0, 10);
if (!grouped[key]) {
grouped[key] = {
display_posted_at: dayjs(post.posted_at).format('M/DD H:MM'),
trimmed_text: trimText(post.text),
postOfType: { mastodon: undefined, twitter: undefined, bluesky: undefined }
};
}
grouped[key].postOfType[type] = post;
});

return Object.values(grouped);
}

const result = groupByText(succeededPosts ?? []);
console.log(result);

return result;

return succeededPosts.map((p) => {
return {
display_posted_at: dayjs(p.post.posted_at).format('M/DD H:MM'),
trimmed_text: trimText(p.post.text),
postOfType: {
mastodon: p.type == 'mastodon' ? p.post : undefined,
bluesky: p.type == 'bluesky' ? p.post : undefined,
twitter: p.type == 'twitter' ? p.post : undefined,
}
}
});
}

export const postToSns = async (text: string, imageDataURLs: string[], options: { reply_to_ids: {
mastodon: string,
bluesky: string,
Expand Down Expand Up @@ -72,6 +158,9 @@ export const postToSns = async (text: string, imageDataURLs: string[], options:
return { errors };
};




async function url2File(url: string, fileName: string): Promise<File>{
const blob = await (await fetch(url)).blob()
return new File([blob], fileName, {type: blob.type})
Expand Down Expand Up @@ -156,10 +245,98 @@ async function findUrlInText(rt: RichText): Promise<string | null> {
return null;
}

const loadMyPostsBluesky = async (): Promise<Post[]> => {
try {
const agent = new BskyAgent({
service: bskyEndpoint,
});

// resume session
const sessionRes = await agent.resumeSession(postSettings.bluesky?.data?.sessionData!);
const did = sessionRes?.data?.did;

// refresh tokens
await agent.refreshSession();
postSettings.bluesky = { type: 'bluesky', title: 'Bluesky', enabled: true, data: { sessionData: agent.session as AtpSessionData } };
savePostSetting(postSettings.bluesky);

const res = await agent.getAuthorFeed({ actor: did });

return (res?.data?.feed ?? []).map((p) => {
const post = p.post;
const postid = post.uri.substring(post.uri.lastIndexOf('/') + 1);
const url = `https://bsky.app/profile/${post.author.handle}/post/${postid}`;

const posted_at = (post.record as any)['createdAt'] ?? post.indexedAt;

const text = `${(post.record as any)['text']}`;

return { text, url, posted_at };
});
} catch (error) {
console.error(`postToBluesky -> error:`, error);
return [];
}
};

const loadMyPostsTwritter = async (): Promise<Post[]> => {
try {
const settings = postSettings.twitter!;
const token = settings.token_data.token;

const res = await fetch(`${Config.API_ENDPOINT}/twitter_posts`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: JSON.stringify({ token }),
});

if (res.ok) {
const resJson = await res.json();
console.log(`FIXME 後で消す -> loadMyPostsTwritter -> resJson:`, resJson);
return resJson;
} else {
return [];
}
} catch (error) {
console.error(`loadMyPostsTwritter -> error:`, error);
return [];
}
};

const loadMyPostsMastodon = async (): Promise<Post[]> => {
try {
const settings = postSettings.mastodon!;
const host = settings.server;
const token = settings.token_data.access_token;

const res = await fetch(`${Config.API_ENDPOINT}/mastodon_posts`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: JSON.stringify({ host, token }),
});

if (res.ok) {
const resJson = await res.json();
console.log(`FIXME 後で消す -> loadMyPostsMastodon -> resJson:`, resJson);
return resJson;
} else {
return [];
}
} catch (error) {
console.error(`loadMyPostsMastodon -> error:`, error);
return [];
}
};


const postToBluesky = async (text: string, imageDataURLs: string[], reply_to_id: string): Promise<boolean> => {
try {
const agent = new BskyAgent({
service: 'https://bsky.social',
service: bskyEndpoint,
});

// resume session
Expand Down Expand Up @@ -377,9 +554,13 @@ const postToBluesky = async (text: string, imageDataURLs: string[], reply_to_id:
reply
};


// const res = await agent.getAuthorFeed({ actor: did });
// console.log(`FIXME h_oku 後で消す -> postToBluesky -> res:`, res);


const reso = await agent.post(postRecord);
console.log(`FIXME h_oku 後で消す -> postToBluesky -> reso:`, reso);
console.log(`FIXME h_oku 後で消す -> postToBluesky -> reso:`);
return true;
} catch (error) {
console.error(`postToBluesky -> error:`, error);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/MastodonConnection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
console.error(`onApplyMastodonAccessToken -> settings:`, settings);
return;
}
const url = `https://${settings.server}/oauth/authorize?client_id=${settings.client_id}&response_type=code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=write`;
const url = `https://${settings.server}/oauth/authorize?client_id=${settings.client_id}&response_type=code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=read+write`;
// url を別タブで開く
window.open(url, '_blank');
Expand Down

0 comments on commit bfcd284

Please sign in to comment.