pin-diary-4
使い方
お試し
以下を自分のページのscript.jsに貼り付ける
code:js
await import("/api/code/villagepump/pin-diary-4/script.js");
自分以外が編集できるprojectからscriptをimportするのは危険なので、常用する際はこのコードをコピーして使ってください 実行内容
以下を井戸端にアクセスした直後に実行し、以後井戸端にいる間1日1回の周期で同様に実行する
今日の日記ページがPinされていなければPinする
今日の日記ページ自体が見つからなかったら新しく作ってそのページに移動する
今日以外の日記ページがPinされていればPinを外す
今までとの相違点
このUserScriptを使っていない人にもPinが反映されます
実装方法を完全に変えた
今回の方法
scrapbox.ioにPin留めの指示を出す
ちなみにこれを応用すれば、任意のページを削除することもできるようになる
これは便利そう!yosider.iconblu3mo.iconerniogi.icontkgshn.icon
2022-02-02
2022-01-19
07:06:42 相対パス→絶対パス
2021-11-11
2021-11-10
20:26:24 日付ページが見つからなければ新規作成するようにした
known issues
これめちゃくちゃ便利なんですが、1つだけ自分ではわからないことができたので質問させてくださいtkgshn.icon
Option+Tで入力できる日付の記法が2022/2/19なのに対し、これで生成されるのは2022/02/19になってしまいます
一番最初に日記を始めたときのformatをそのまま使い続けている
このままだと、ぱぱっと書いてる他のメモと日程表記が合わないので、できれば2022/2/19の方に統一したいです
変更する部分を教えてほしいです
日付処理があちこちに分散しているのよくないですね……
ここは直したい
根本的には、両方とも関数の外部から注入できるようにすべき
code (before bundle)
全てのコードがこのページ内で完結しています
外部コードに一切依存していません(型定義以外)
このtemplate内でdate-fnsを使用しています
code:script.ts
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom"/>
import {listDiaries} from "./list.ts";
import {togglePin} from "./pin.ts";
import {getTemplate} from "../日記ページのtemplate/diary.ts";
import type {
Scrapbox,
declare const scrapbox: Scrapbox;
const targetProject = 'villagepump';
const handleChange = () =>
scrapbox.Project.name === targetProject ?
startObserve() : endObserve();
// initialize
handleChange();
scrapbox.addListener("project:changed", handleChange);
let updateTimer: number | undefined;
async function startObserve() {
endObserve();
const {id: userId} = await res.json();
const res2 = await fetch(https://scrapbox.io/api/projects/${targetProject});
const {id: projectId} = await res2.json();
pinDiary(userId, projectId);
updateTimer = setInterval(() => pinDiary(userId, projectId), 24 * 3600 * 1000);
}
function endObserve() {
clearInterval(updateTimer);
}
async function pinDiary(userId: string, projectId: string): Promise<void> {
// 今pinされている日付ページを調査する
const diaryPages = await listDiaries(targetProject);
const pinnedDiaryPages = diaryPages.filter(({pin}) => pin > 0);
// 今日以外の日付ページを外す
for (const page of pinnedDiaryPages) {
if (page.title === toYYYYMMDD(new Date())) continue;
await togglePin({userId, projectId, ...page});
}
const todayDiaryPage = diaryPages.find(({title}) => title === toYYYYMMDD(new Date()));
このときStreamにいるとURLが壊れるバグがあったので直したtakker.icon
code:script.ts
if (!todayDiaryPage) {
const a = document.createElement("a");
a.href = `/${targetProject}/${
encodeURIComponent(title)
}?body=${
}`;
document.body.append(a);
a.click();
a.remove();
新しい日記ページが生成されるまで待つ
code:script.ts
await new Promise<void>(
(resolve) => scrapbox.once("page:changed", resolve),
);
code:script.ts
return await pinDiary(userId, projectId);
}
code:script.ts
if (todayDiaryPage.pin > 0) return; // すでにPinされていれば何もしない
await togglePin({userId, projectId, ...todayDiaryPage});
}
function toYYYYMMDD(date: Date) {
return ${date.getFullYear()}/${zero(date.getMonth() + 1)}/${zero(date.getDate())};
}
function zero(n: number) {
return String(n).padStart(2, '0');
}
Pinの付け外しをする
code:pin.ts
import type {PageMetaData} from "./list.ts";
import {createWS} from "./socket.ts";
export interface PinProps extends PageMetaData {
userId: string;
projectId: string;
}
const MakeTogglePinRequest = ({pin, commitId, pageId, userId, projectId}: PinProps) =>
`422${JSON.stringify([
"socket.io-request",
{
method: "commit",
data: {
kind: "page",
parentId: commitId,
changes:[{
pin: pin > 0 ? 0 : Number.MAX_SAFE_INTEGER - Math.floor(Date.now() / 1000),
}],
cursor: null,
pageId,
userId,
projectId,
freeze:true,
},
},
])}`;
export async function togglePin(data: PinProps) {
const {send, receive, close} = await createWS(
"wss://scrapbox.io/socket.io/?EIO=4&transport=websocket"
);
const stream = receive();
await stream.next();
// 最初の通信に返答する
await send("40");
await stream.next();
// Pinを付け外しするよう命令する
await send(MakeTogglePinRequest(data));
await stream.next();
// 全部の応答が返ってきたら閉じる
await close();
}
日付ページの一覧を取得する
code:list.ts
export interface PageMetaData {
title: string;
pin: number;
pageId: string;
commitId: string;
};
interface Page {
title: string;
pin: number;
id: string;
commitId: string;
}
export async function listDiaries(project: string) {
const pages = await fetchPages(project);
return pages.flatMap(page => /\d{4}\/\d{2}\/\d{2}/.test(page.title) ?
[]
);
}
2021-09-18 04:38:04 本当にちゃんと全部のページを読まないとだめだ
2021/09/18が1000ページ前に埋もれていたみたいで、Pinし損ねた
04:49:24 全ページを読み込むように変更した
code:list.ts
async function fetchPages(project: string) {
const res = await fetch(
https://scrapbox.io/api/pages/${project}?limit=1
);
const { count: pageNum } = await res.json();
const limitParam = Math.min(pageNum, 1000); // APIで一度に取得するページ数
const maxIndex = Math.floor(pageNum / 1000) + 1; // APIを叩く回数
// 一気にAPIを叩いてページ情報を取得する
const results = await Promise.all(
.map(async (index) => {
const response = await fetch(
`/api/pages/${
project
}/?limit=${
limitParam
}&skip=${index * 1000}`
);
const { pages }: {pages: Page[];} = await response.json();
return pages;
})
);
return results.flat();
}
code:socket.ts
export type Return = {
close: () => Promise<Event | undefined>;
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
receive: () => AsyncGenerator<MessageEvent>;
};
export function createWS(url: string, protcols?: string | string[]) {
return new Promise<Return>((resolve, reject) => {
const socket = new WebSocket(url, protcols);
const once = (
type: keyof WebSocketEventMap,
callback: (event?: Event | MessageEvent | CloseEvent) => void,
) => {
const wrapper = (e: Event | MessageEvent | CloseEvent) => {
callback(e);
socket.removeEventListener(type, wrapper);
};
socket.addEventListener(type, wrapper);
};
once("error", reject);
once("open", () =>
resolve({
close: () =>
new Promise((res) => {
socket.close();
once("close", (e) => res(e));
}),
send: (data) => {
socket.send(data);
},
receive: async function* () {
while (true) {
const response = await new Promise((res) =>
once("message", (e) => res(e))
);
yield response as MessageEvent;
}
},
}));
});
}
source code
type checkにはdeno bundleを使った
code:sh
2021-11-11
12:54:22 ページが重くなるので止めた
08:32:20 やっぱsource map貼ることにしてみた
2021-11-10
20:25:23 source mapは削りました
むちゃくちゃ長くなってしまったので、そのまま貼り付けるとStreamを壊してしまう
code:script.js
async function F(e){return(await B(e)).flatMap(r=>/\d{4}\/\d{2}\/\d{2}/.test(r.title)?{pageId:r.id,title:r.title,commitId:r.commitId,pin:r.pin}:[])}async function B(e){let t=await fetch(https://scrapbox.io/api/pages/${e}?limit=1),{count:r}=await t.json(),o=Math.min(r,1e3),s=Math.floor(r/1e3)+1;return(await Promise.all(...Array(s).keys().map(async c=>{let u=await fetch(/api/pages/${e}/?limit=${o}&skip=${c*1e3}),{pages:g}=await u.json();return g}))).flat()}function L(e,t){return new Promise((r,o)=>{let s=new WebSocket(e,t),i=(c,u)=>{let g=y=>{u(y),s.removeEventListener(c,g)};s.addEventListener(c,g)};i("error",o),i("open",()=>r({close:()=>new Promise(c=>{s.close(),i("close",u=>c(u))}),send:c=>{s.send(c)},receive:async function*(){for(;;)yield await new Promise(u=>i("message",g=>u(g)))}}))})}var G=({pin:e,commitId:t,pageId:r,userId:o,projectId:s})=>422${JSON.stringify(["socket.io-request",{method:"commit",data:{kind:"page",parentId:t,changes:[{pin:e>0?0:Number.MAX_SAFE_INTEGER-Math.floor(Date.now()/1e3)}],cursor:null,pageId:r,userId:o,projectId:s,freeze:!0}}])};async function A(e){let{send:t,receive:r,close:o}=await L("wss://scrapbox.io/socket.io/?EIO=4&transport=websocket"),s=r();await s.next(),await t("40"),await s.next(),await t(G(e)),await s.next(),await o()}function n(e,t){if(t.length<e)throw new TypeError(e+" argument"+(e>1?"s":"")+" required, but only "+t.length+" present")}function a(e){n(1,arguments);let t=Object.prototype.toString.call(e);return e instanceof Date||typeof e=="object"&&t==="object Date"?new Date(e.getTime()):typeof e=="number"||t==="object Number"?new Date(e):((typeof e=="string"||t==="object String")&&typeof console!="undefined"&&(console.warn("Starting with v2.0.0-beta.1 date-fns doesn't accept strings as date arguments. Please use parseISO to parse strings. See: https://git.io/fjule"),console.warn(new Error().stack)),new Date(NaN))}function _(e){n(1,arguments);let r=a(e).getFullYear();return r%400==0||r%4==0&&r%100!=0}function v(e){n(1,arguments);let t=a(e);return String(new Date(t))==="Invalid Date"?NaN:_(t)?366:365}function q(e){n(1,arguments);let t=a(e),r=new Date(0);return r.setFullYear(t.getFullYear(),0,1),r.setHours(0,0,0,0),r}function D(e){let t=new Date(Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()));return t.setUTCFullYear(e.getFullYear()),e.getTime()-t.getTime()}function w(e){n(1,arguments);let t=a(e);return t.setHours(0,0,0,0),t}var J=864e5;function Y(e,t){n(2,arguments);let r=w(e),o=w(t),s=r.getTime()-D(r),i=o.getTime()-D(o);return Math.round((s-i)/J)}function T(e){n(1,arguments);let t=a(e);return Y(t,q(t))+1}function d(e){if(e===null||e===!0||e===!1)return NaN;let t=Number(e);return isNaN(t)?t:t<0?Math.ceil(t):Math.floor(t)}function l(e,t){n(2,arguments);let r=a(e),o=d(t);return isNaN(o)?new Date(NaN):(o&&r.setDate(r.getDate()+o),r)}function M(e,t){n(2,arguments);let r=d(t);return l(e,-r)}function $(e,t){n(2,arguments);let o=d(t)*7;return l(e,o)}function x(e,t){n(2,arguments);let r=d(t);return $(e,-r)}function b(e,t){n(2,arguments);let r=a(e),o=d(t);if(isNaN(o))return new Date(NaN);if(!o)return r;let s=r.getDate(),i=new Date(r.getTime());i.setMonth(r.getMonth()+o+1,0);let c=i.getDate();return s>=c?i:(r.setFullYear(i.getFullYear(),i.getMonth(),s),r)}function h(e,t){n(2,arguments);let r=d(t);return b(e,-r)}function P(e,t){n(2,arguments);let r=d(t);return b(e,r*12)}function O(e,t){n(2,arguments);let r=d(t);return P(e,-r)}function E(e){return n(1,arguments),a(e).getDay()}function k(e){return n(1,arguments),a(e).getFullYear()}function p(e,t){for(var r=e<0?"-":"",o=Math.abs(e).toString();o.length<t;)o="0"+o;return r+o}var Q={y(e,t){let r=e.getUTCFullYear(),o=r>0?r:1-r;return p(t==="yy"?o%100:o,t.length)},M(e,t){let r=e.getUTCMonth();return t==="M"?String(r+1):p(r+1,2)},d(e,t){return p(e.getUTCDate(),t.length)},a(e,t){let r=e.getUTCHours()/12>=1?"pm":"am";switch(t){case"a":case"aa":return r.toUpperCase();case"aaa":return r;case"aaaaa":return r0;case"aaaa":default:return r==="am"?"a.m.":"p.m."}},h(e,t){return p(e.getUTCHours()%12||12,t.length)},H(e,t){return p(e.getUTCHours(),t.length)},m(e,t){return p(e.getUTCMinutes(),t.length)},s(e,t){return p(e.getUTCSeconds(),t.length)},S(e,t){let r=t.length,o=e.getUTCMilliseconds(),s=Math.floor(o*Math.pow(10,r-3));return p(s,t.length)}},R=Q;function S(e){n(1,arguments);var t=a(e);return!isNaN(t)}function j(e,t){n(2,arguments);let r=a(e).getTime(),o=d(t);return new Date(r+o)}function C(e,t){n(2,arguments);let r=d(t);return j(e,-r)}var X=/(\w)\1*|''|'(''|^')+('|$)|./g,K=/^'(^*?)'?$/,ee=/''/g,te=/a-zA-Z/;function m(e,t){n(2,arguments);let r=a(e);if(!S(r))throw new RangeError("Invalid time value");let o=D(r),s=C(r,o),i=t.match(X);return i?i.map(u=>{if(u==="''")return"'";let g=u0;if(g==="'")return re(u);let y=Rg;if(y)return y(s,u);if(g.match(te))throw new RangeError("Format string contains an unescaped latin alphabet character "+g+"");return u}).join(""):""}function re(e){let t=e.match(K);return t?t1.replace(ee,"'"):e}var f="yyyy/MM/dd",U=e=>[m(e,f),[${m(M(e,1),f)}.icon]←${m(e,f)}→[${m(l(e,1),f)}.icon],[[${ne(e)}曜日]],${k(e)}年 ${(T(e)*100/v(e)).toFixed(2)}%経過,[🌏 https://ja.wikipedia.org/wiki/${m(e,"M月d日")}],[[${m(x(e,1),f)}.icon],[${m(x(e,2),f)}.icon],[${m(x(e,3),f)}.icon],[${m(h(e,1),f)}.icon],[${m(h(e,2),f)}.icon],[${m(h(e,3),f)}.icon]].join(" "),"今日のn年前", [${m(O(e,1),f)}],"","","",[${m(M(e,1),f)}]←${m(e,f)}→[${m(l(e,1),f)}]].join(` );function ne(e){switch(E(e)){case 0:return"日";case 1:return"月";case 2:return"火";case 3:return"水";case 4:return"木";case 5:return"金";case 6:return"土"}}var I="villagepump",W=()=>scrapbox.Project.name===I?oe():H();W();scrapbox.addListener("project:changed",W);var z;async function oe(){H();let e=await fetch("https://scrapbox.io/api/users/me"),{id:t}=await e.json(),r=await fetch(https://scrapbox.io/api/projects/${I}),{id:o}=await r.json();N(t,o),z=setInterval(()=>N(t,o),24*3600*1e3)}function H(){clearInterval(z)}async function N(e,t){let r=await F(I),o=r.filter(({pin:i})=>i>0);for(let i of o)i.title!==V(new Date)&&await A({userId:e,projectId:t,...i});let s=r.find(({title:i})=>i===V(new Date));if(!s){let[i,...c]=U(new Date).split( ),u=document.createElement("a");return u.href=../${I}/${encodeURIComponent(i)}?body=${encodeURIComponent(c.join(`
))},document.body.append(u),u.click(),u.remove(),await new Promise(g=>scrapbox.once("page:changed",g)),await N(e,t)}s.pin>0||await A({userId:e,projectId:t,...s})}function V(e){return${e.getFullYear()}/${Z(e.getMonth()+1)}/${Z(e.getDate())}}function Z(e){return String(e).padStart(2,"0")}