From 8dd4dd32efbb99a6e47a511c8db0cda17365d233 Mon Sep 17 00:00:00 2001 From: Enki Date: Thu, 21 Nov 2024 01:05:37 -0800 Subject: [PATCH] Initial commit --- README.md | 21 + admin/settings.php | 2 + assets/css/admin.css | 39 ++ assets/js/admin.js | 63 +++ bluesky-connctor.zip | Bin 0 -> 15143 bytes bluesky-connector.php | 805 ++++++++++++++++++++++++++++++++++++ includes/bluesky-api.php | 70 ++++ includes/bluesky-auth.php | 152 +++++++ includes/post-formatter.php | 243 +++++++++++ includes/settings.php | 2 + templates/post-meta-box.php | 58 +++ templates/settings-page.php | 167 ++++++++ 12 files changed, 1622 insertions(+) create mode 100644 README.md create mode 100644 admin/settings.php create mode 100644 assets/css/admin.css create mode 100644 assets/js/admin.js create mode 100644 bluesky-connctor.zip create mode 100644 bluesky-connector.php create mode 100644 includes/bluesky-api.php create mode 100644 includes/bluesky-auth.php create mode 100644 includes/post-formatter.php create mode 100644 includes/settings.php create mode 100644 templates/post-meta-box.php create mode 100644 templates/settings-page.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d51920 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Bluesky Connector Plugin for WordPress + +## Description +This plugin connects your WordPress blog to Bluesky and allows you to format and publish posts to Bluesky whenever a new post is published. Users can log into their Bluesky account via the plugin, and the plugin will automatically generate and store the API key. + +## Installation +1. Upload the `bluesky-connector` folder to the `/wp-content/plugins/` directory. +2. Activate the plugin through the 'Plugins' menu in WordPress. +3. Configure the plugin settings in the WordPress admin under 'Settings' > 'Bluesky Connector'. + +## Configuration +- **Client ID**: Your Bluesky client ID. +- **Client Secret**: Your Bluesky client secret. +- **Redirect URI**: Your Bluesky redirect URI. + +## Usage +- Once configured, click on the "Login with Bluesky" button to authenticate and generate the API key. +- The plugin will automatically publish a formatted version of your new posts to Bluesky. + +## License +This plugin is licensed under the [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html). \ No newline at end of file diff --git a/admin/settings.php b/admin/settings.php new file mode 100644 index 0000000..1b9d02c --- /dev/null +++ b/admin/settings.php @@ -0,0 +1,2 @@ +$gNrHj_fPOVq2xs+woc!wp;{99S$lBb7UhW?>AV5$+H1YQ8^699o z((nIiz8@(6rg1cOax%9ub)>U1v#YPAAEDTsT~vh{9idSX8ygo_2AWNEALS^dET#Mu zA|NhOCSXGpD<7+vhEVXy!U+&@g?8yjfmTvPf?uRVSTB^z-bheOQA@&7i~{seGqvK76b_B*C6~$+AqR?TshtQ zS>l&on|=Z$q4QUmg0g0X)^Nqb$wfUi1{mOmibN&lx6Y-dQe$Q1F?+Sn)Xd5(F1Amc z-B*|>KZOE926(BGbO93Twi>qz!>gr_6=hF$soUnUD6>rTfv`(;p?i&%2sh@lP8S2! zdeXAA#{r6b4nT~XA$dDK%^NSFD7~NE**;XZH$U2%7hyifv7tctggYXeRnT|VV0qIY zGq|u&WyYEY>R(t$s$?0WQ)w*=Pb)?Qeyqn{oaIbdJdKSlXupAR%>5 z6k{TFx7R7{zPa#ZsF4r5NS;E7$AJ8LyF||cPu$mVhjd#iQLYpv%vz)qZ3|MFo__X_}Q(0vn(2>*4l6rHKqx{UMbU z9K{%R-(XrSz_mIA>OBjUm$RQ)dN?(WA-roX8y8iYx{xp3GcXmD<)L=%MY04=tuUjH z^gKWXwQfrl*;%)#b^;zOutJZV+CVV~P!FQLn0K~oaUv=1TJde5v$8&U-K@Ktk#2h~ zYqyV$D%eW<1h&2as_y{AXP$YB^AB8STU>)1ljm&!Xtodr{N!fn=9J=(gLlMQpSiQ2 zMt*oNt*v@q&s$urZ$XG;V%)u9G^1T~wBW02C7pCM7kiFP{SOO(ezyQ~8$&B+Bjdjs zfM|@PdTQDuvg-TIx0SYt6xy^WQXX|mtrc~oYDcpD{we-IciNhQzK-b6Y^cNa^dy4_#~vF0~F zPpiYV(7T=0fK4V9UPeTt(WruGWh243ub$gFiMt4bu*I!LWzPoivI_+?qdb^{?6IbY z)LYa9QBNV1C=9viB4aDvK|Vn)JZoqQ6&PzO^@?%v-inU&&e9aSZ7%Dq-;0ZPxXFxo zj%$*-;%uDfvWd^lGv%SkLblZytnl~89AL$wuup5O+!T7b(-SKEplc9jQW`6yr^xk5 zU8AcT5_Q%6BuDl_NHq1(X~f0sd)cy!x@Hw~3>9XF$r>M(zGpZH?RST<-ul!w@kxM1$OKTdr|9q7OQ( z58iMR7JQnYiFUKzbNJKv;ikjLuGGzpU|8b;EX*70G8xw-=qC z6f_36fnIp6z^vWopax!e!2q$JV0xCrggO#ioYX$_`Me!fJThT-V3~=4Vh=-dXKncD z=i1NfOXbcA#LGC#iS-2d1N!3=XTzkEYS55wjc`~SKimiqDq?hyd7AYS`74F8yykJs zFK7;f!Yo`hToWAt4ozldo1hA{q}{q%5tdeQ&};ipb_dq!^J$u3Ue421Zq`q>x^AV6}?$}TMKsDTXXRrwU%EH?cf5Oo)9!?JZ zVb6^Cns8k_PBOS3?GkJ-40m-y174g@;ag2xMcD~4sBW=L5)~k*qfO8+@WqE*DQx={ z3Hu)PFcRX4KNQl~_v(^bMNi_JRA zEIh&c5+}qgAnbay1dL#0#j~V-(an+QuRwwSkfrmrh=vIq7Tn{|_z57Vhpp_6(_t5d zk3uL$XKfNqRl3x~6wT48;LJO*YtaYqFfbAh;S6jMN8O|IlN!(O?0eE3&(^~VCKdTt zp4%iCtCXngnH!fF6aO2@O>m0nCda~i&gg)+j>N=Xs}J5+h8uAgNT0q zVw>z7@{nKAiq0-dB2R0S)^bxbfyNZU)k}B!L+6WmCc0%HpiPN<69qbKjmif@1{q5MV?7J5CANBvL)oO z5_=L~VtlvT@Pi5Cu_+veE2ck~+y9a+CiikTaQlW&FMT;2#^c_%rR^$tvO*{1f9$=- z!gAKPKzm}7HkGXOoTl0rRvnAg!n8z_LlZ)CG-Cl-*F9zv9P@n{FBwBS*$)N6i4CLQY#|A%NiWbUo;kU-viI1Bx}-hg%rt z=4{rrrXBpm+{(qm&bQtwTRh>##@#H8FHETK;uSqm5TU6J)FH@KkvVe_SDH_)%T>svQJeIX<1wzViJ1XsDw~&&7jF$^ z^oKcX)J}Jp@C6!1dY56}I+5^Yb(0nrdh3=tOH`>tL<#1 z#~QQ-u@=)h=^Mh=(Um8okR@dM$)TVHP zUB5qAHI)y-D%UfZr6ZkikByCb&zOTNkhY5`VH_?I#42wM^5+meoK}cCd(xh>Q5;yO zum06gf$U36HC2>ROL&@Dp|0@dzE0cL9J|I~=|Fu1vwJTd(#$e|DYj`rs-2s$KCAA%Uuba>< z56Z|@@Q9=%0*?jG?QZf(@Vo}*+r1z`Sy@TIXJYXX72?Xu8P03oecAFe&8J2bf`&_n zy`u$@FpN``KA)tPYwI~9%Doq(S_T3#A^_=DNN+DG1!FxD(q~{w2jha#fkd|`Vdb=P zsj~K-kB1s%?B%G)$V+f*cyMwdOhiRY$O4C^`YB4`9m(PoAHz~QXaFU7otIne@M_&A zJ}XQ0sx(mC>X|4n5$c@o_Ihr5f_KcO& zA2fs>l*Wcg0ORt@KfhKYyeh;is{Ix<;;h6_;a}mFu<|hx|2T+MrX*zltcdX>#`|aJ zqo8-KZZxPXg{0wh!bAaukC3TYS^ z8C`S~pb9N9`TS}Ylp$WxEz(B4B0n&+ySfqRy{> zSai@v>)}t99HitSc{+`O%CsbROx@?<@UsD0d|F{7O9L19AmlY;E*9CNs%H@$n%)C`le#wrcK4kHo$ zUz<6s{4|_Iib-3^qM-D>`)xY`m-v*^pf>HcP?P7k(t{QR`vz$B@T+uFaGkbppxO|F zBM21Q=&=3mU?F(e{F+TS-a!3Qx}Tu^9icgB-*{5;wj}zqAYrH7!Nio@REjX~zn+O! znl{*L6#i2vXf_doB0+II?)ws&R&~|U0yZvcm;*q|MvT^By%S=B1kGSHm2$z$wwVjo| z(|-gwnn0Y@>(0)Q6y77F@`V4~|NNo*-9uVCeN*FK`=2#+&G-Eez~{W09c@Ko$e{gj zE_MZarEnF-NxjL1B19j_>~M*QD3VHi@Mrf1GUaj{(P%QOH6vuOcKCjur}cKDsC>q-*DQ;S9)Z1*E$rD* z;zLlH@FHnby`E8HO4Xg@FpYbo0x#PTMB)t7S|nYwQl?*DTp9d6Di{bIZ@oA$WXj}v z`RA_687e6kr9e%9f<3V)sUzhm$ar^r5sy54I%qvVt#L;mwp>a9q1|-O$Zb>SbCK=I zT;Aw6@QbxUPHOluneuEd127B@b&FAeS>AW0a?jh(I`=%2O)v3B<_at>>;DmNT|})< z4%uKUbj6L!P)f3MmAqS!HigGxiphYbQp6mtqvRl$I`Gt2yPgw&p)#j6LP{eqN|eD_ zDz^e`;v}6Z0$16qd_+zJJ)P|sC0O0CWm|D?Y**3R29s5g3ehQJeDvh=M}&gVJY> z-Ko-k7V?rCpy#8SIX=EESW*hx zk~(D7M30p-2On=e+)bO};NP9py$q{zS^JjeujH;INJ>;4wDLbetoNhs}a(6r0){yt+O!j`v)IdSjV!x=$?{ zxc(94i*bxmbTl6p)mUqkp50fn237*<3asl_ahG?%FSZh11OWKj7@RN*zQt4u7kwU_ z^O0H$FE{b)OBrYBZQOFeMJ#t*4z^*aMU!(ta*SadXa_Pb3uTnG$&f}HDEc}v8j7}} zA~0yu#A?OFiaK@Du1Y~?Qsyvj=Jf(;27zF!ooOQEFw zH3l{%945Gw+U~YfozQIN6PdqUu-^eSm;+Oj{W9ZGm~t&%0Cnz9I9`zyAlU$RSm=}< zA@xiv6)7Blx4{J%fvT{pDt^%x<{vqgAA-RKzCVwniX^!3L*eBsJQ${T5l!uE94s9; zSJkc^Td~b}xurDxq>e`;-C0X)4x9i4?DvY5eHN1UItLB-xDZa7fq_Ut% zlUqjEU$pp1aw#Dd%XL$60!^YY2@BzNAP)vXowmt6ib`!4SKUcQUx?p)cjRoDcr6=| z0z!ge6k5`)E;z_Qg(DVehRy_#1jHnm=O&K8ql`J){9yx9ts(9zGsBAyd&L2u^qy83Isz9TS9$f6jqbiRs-7nv`EYyE zKQv;}F-)5~-DSbqa+lLU#- zWj<o@pmnQzucQHAh! zgln6X#Kni{FX@}VGMo2+Lhael6ScdWQ9Y^Z7FlP@!TUIU76xlgeTo0aSIWF6TVy+~ zylviN?XU0g_+O9We_YW&p2e+=o%Cr9Y~6mjqQ)f2Us;#o^A|vd{3irCwk7M4@}6m2 zkJT$03RYB{TGFY!Wboszr)xVDF8c_w39mZZi03PEg;|nOAN~y3%Hm$`YWZqb`BG-# zG_Y@+DQkZ2WQfKZeQi4mqnHmw{oK&%hEtV1^s!*sdaDQBxioD~ww^G3?FGn0w2+GmUQ4w=dm9F^(JEt=*wt zKTl*KZ$WaFNiM70l_+cER(a}2%%Huq-7jC0>sJw7b4u%N+_pZnh$RoW$iaAetnDWE z#?9V{_oPHvMi(+0yf}G*#G?DsLPe>_WOQ~epd3u_8HA>@3EX&aeG5TKOyO3SZLHVF z_NZ{4g6XE=05dCZxPHgL0iTDi2JFY-Pl2FX3A+VIhvF$NJC{ZA@7742=J-h+^-(bO zz!9Vn)&5&9smIES`8So3>y7~tOADC@9x~Xnqs?4W#dD`2a8py$8Gfschq(ve$1RCS z{aH!VV$X<|C@%~(3DFdG5wC%yqG5fTOh=j7fr!GW@Hx_WcsR*;BUcgncXQX?0VHLd^3^!SWmprXYbl`PQsfFk z@a>to9y}54KD)^}tik(%iOb~~P0dSvD{68v205^0#*T)#9o%3mmp}t>95!^l zP1CJQwi)lA(qxS)u0pMH>p}PFS%2vD!n|OpaCT#d&5+)?UnjLJhPNWl(L0aEJa&_8 z`q@um^d-B;+b$0+eNca=I#c(wbj*HWC5Gr=&0umx$DygNvsKc!AMsXl+;DQC2~yhf zOnASsnHviF4RL=X+gI0ec;+i2ojRM2!{>Teudkf1G?$>ueP^Pbl1&kL(r)hdy`_tw z*^{MH597LvkCRcJgm!O5Z1TQFq7OLtd&9j^*-yQfeQ4f&upbzH=o0s|G~p7MQ{8i? zKV%JuL(TMsxHPmGVN{*c8g7zQe6x9;V6{ss4`VTrzTLj#dkysuUWtXYBp32YMP-tM z%C=c%s_6`y(zHUhn|uaOuRPHk)ub?AU9E!`gMzr9gOba~dyhm}u^eDk0!bav=RtC9 zlIU^4y`&m~azikM%MY<0CDVRL4y{10Waq*fLnfCNVA^le1g)x(X8B_C!fv34D8D(5 z1W_`d-um-{x!gomy$cD%2N@zLOCV+hf6#*Z!G+iF>YHy429g>|krTLJJkiAWm|LQ= zpjcnQ%{X_eG(n*drlmWI=AjKiXm9{#B*H>X4>gyI>M|KMO<|yjPXtVT_s|Iu zx;0OIuG(+F>Y&zBxH}Xb72n9FjbvZwGu)7|Z=a#W&`5GXSW8um6fG%6aC9Yvxo0+h@uYajqJeOW?wrK7g#qI0Bjq0(4Tg?|g-|O1I ztNrm}#*XO<>R0$wA|C44f!I2!P|8LVtktXQuPU3YVCTplZ|%4yxY9{Xto6S6V|ty^qiPZ&$U6>uxj|X z*3@eV*O8%#O&g{FKFyw8vp6{KMI2Yn3ZA2Ad4lHr-gFiCL1UhU$LEC~3ax!qx zY#8p5Yc{2POU54KECl>7}<;hsPmIU1_n3c~0FaW9}cYW+ju z3^Wh`>%u29AOlI=vHAp{6M^J1Qsdt z@Rsxu&vWb-nz99_{Jxn1w?(9?kO9q?#GlB8)kss( zt7?AcI5^;2F+k4D_lJp2d=gD;6h$ziu$nGKTbCaJ&x4?&q-0pGyI!F+I@P9JI)xo& zk8xoF!JN1|iW77XK!2t*iM{_M;ES7z6BT&!(IcywtW-7>tB2#t1WSYxNHFZ`U?-Fi zH}mLaJYBS3?1U4!+9I0x%=iAGOCXGUpe5e{x*=ySn&wRj{T6I<`K6_>4?P8K((WPu(kTNSZE7@}~g1IxRp!g)B%;7AIAo9xi+n7mwdUzLm+Yf1eHY zCYQej4;2=syLC~4?|@{(1b*^tW7j7$IV z$q}{}+(|{JG0*X_eje?~QnV08B0?(2lH*tGmWS8eXr_lm&5uhRGN;R?*rjf^p9mVl z^E(D57HnUDK&Zb@N>s4p-g}^0!*<$d$wda3fs_X8i_H2`Mctbal^+M&_rz-9lEQ_^iQ0{redqe8)-5DoD03)du5%vaoQ80XA_FJqyxK$Wg|P}@PD>be#D>5?%t_aYTBxwnzaJy+5|?NJ>dFSB1R(qeA1S(ZjFg@8lK{YbYB$Y zx%(j03677#l8w`r!|}=nO@8RCGm0%djz9Osu2bXe1)@+4oQr+9w(V0A$;ousNU=D} z4&N-L2G9EVX;^V8E9vJBA{VL|f~gFT0zBm=8A=H~+XSZQ;^^7BNa&_HBRp*^He;rN z!Ds*;bwv0y#olt%Xi>-0w7?LPU#gXOCy;*9Nh}TbO;o?ar@<^Tw<>r?f0|A}>_RXB zX0m82Lo&XRs8^>wzx1K_E!HEUdxrMJ+$ebX#9Tl_;D_v$`dx4Q>`E^c-`dup9h{yf zm6?@6bLjH^K6L{kB!iY6ij2ZMY4dc4MPG{aW(q$H3?N^`;=Z&2nRXb{$J>&gZ-*E- z6u|YoeKx*d$3%8083pZ~Q6SHt`?sUZ<7rw@MJTFe5a>_?R1L~2;DO$NDS$jK;BV3!`x3G@bx%2L9 zFzY3Ga&A_<4~v5m0yG40{4!oX4bMVJM9wA#!fVyc9MnVhbjn87P31L2l}wI6MQL`G zv;dbdS<22#15Km*I3sbVo-!g07H(W!2Up3wLla4T2l*DSC)qe^-ym>ZKaeMfc(0tK zSc*bl@qHn9RYJMSl3|lD3q=PS({%BGNm#uA?IF8$al}$uq~73rXj_;OFZw)*;gW=Y zg5Q^T5okZDd!?K$*A3sC*oxrOmZ45jsJWF$boj4}g%NoYcG!-iS&24fk>3++1Tb9% zU!nZRKKJw^3sf)q`ZDf+*IVYb%?PTy#o#SoPN^B%QL#a8Kf4uo_2bIqJquB;eVHVk zlk)`EL40%AV^{r=oZF!Pyu7^0JP`ae4gSbm~B54+Nu~8C|PomXsc1eNAK0N>* zUvu6ZQD1=&341mfC}@o%EFYS74X$<9WeU7TG1ImTW{(Dq=%Z!C5i`uU58zey_bDM8 z``qNieLo-63nfk1cL)LSe2932ItQNRxh+)`xi)dA;H$j{xL z#9@Eop9bZ`Vj2`X^Z~4#`-okPeZ^}1JYzE+j-3SBesdllK2a-60#z|w-@6mu*B+K- z43b|4!=o_;D+enujCckUv?WmQ5LsJh*rX(+RhryD``Kr-#0V#lk(yyCTEpb_TadRQ zQszTS9HP&8T6*M6by^eS7jb5(D~f&#G? zSH=Qjgn8n)v7*<-PEp;|IGgQ5)!CNqX?5uOYa`1Hu_xsHN%LKII$N5rW{$g}HLld< z>SbOP%Gya}oH#=a*GeG?hcHE0i_6Z{h7Ag7_8zbP-?M{n72y?Fh2{yq0ILS(TCE91 zB?b|oK_QR%gE>mAWC0P!e2l%CY!}7@_y+YcD)2}9owCr=_26g0%c0xpWSh) zT;$tbt;{REdRIq4IcVbPGlpjvaUT&8t0~Y0S8zR`CNKbceHz&UOG}YFccG6OEjFAU zc>$@{5!V@>GVUrz`42U+>F70a0g$qv6L$gkDcM4#`qtLIQkv1kLjIl`!T6q`byun@ zR7XM^s44T{^D%I!P;O+qANT31G4T(Wjg?3R$C-?w3!Fq`P))NZzt$hRrSG(0rRxYi z+(o^-za=M^h9`M4Kx%mLc%Ry`#x29f)VCyPjWznsjxX018=~&)af;nMX8@70UleGB zM#YXoe}Z`PUEK>-o%)Rc4-WgICx>iX{m_49Mv99u{`(;xk?Zvk>WwzP$U^I5E+}}WKE55k{K}9HP7o^l zdOhVyzMKPS%}-m#@jPmg*)gMHhxbygY`#~Vbnudhu&8o`gqrV+y&d{&x=5kj^JTsd zz5K)2L5K6U9k|RZp`G7%0v8q#j3tv%*2op?r;}E<*6xA4x%5ksbO>EA*a`(I;T2*AOEPJwV!gTLGNMO)e$ZxU?Wm)MnR-adm;mC5RV+Zp9g z1fk_+y#ztco~j~jDhiyHdT2H-Ln)IIy*=TsQ0TLi$shw4b<0e^vmh3%mYdUY^DeNL zkj(}t`g)Ml;KrdLE(f0mPpH&nwsE?nXKC@l>S`@5K9EvHV-y@sUaWQXxGpo?uuRn~a~jjUrbhMM&@CfR2|{;O^=y4rI7^W+-iaYM zb1@<}o|BG$Ay;p76)d0gc7K!DnVZ+$-OmdGbMbAcJ{-TuTtQiS7gS} zh$F_>5#AH6_g4U)vX`{Inr;&2aV*8_D*%ub7pAI~(Q(|JM8QW3Do z_=-=;f}!wqYqD*#Th6af7Z(e}pqUn^{HTPsh65ChB1=fKah7p}z$nj1ze{E-Y><<+ zzNE@3aGw#&x>JWqqFPp4?KE(*IM7e~L?r~p^({>drOz#}n!$o3gDktEfGPM4C1uz4 zD09H+rq7dXi($Eb2U@xcq{w(^-3xB4j6%fBMOS|OjQF$Rf!X#`8kGamR^WreOdISi zDJ5f?>!`cJ?WJbE`eBw|#!W`0pIaL>fle3BJd4Z|){MHm5Vjls;}x2Np4!hYiep>EIXTl&|dIZqVjs!izk$0Y%^ z04nfm;>s8ag3-$){zM{%D^=WY2s1pCq>oE)_|4=(@Dis1s!; z&Vx7{9Rqb3vy*5jsR!~%-A%W!L2y04u65K?!V~B~5gbv033(wg%T1GGSHS}dy;cR~ z#wD=_%ZbW#CXvYTA}=ua3qj7tUY}QO%m;Z8+TnT^Z$dOV>Ti`T1;YOb->cVS%qEj{ z>fd_hVyEwCIjea3>O|3w5$}0R>MCRtb0t2%n2Lkzl*e?_jb1fza1o#WkuSE9ff)ms z+LRUyeuChm7Ofze=JfeUq+ehP#CdeJfiy?Z0bVDBqIqvpx}zAjq*U&GSBx8e@g)dZ8+G&2PeZT5uFr_%-C&D~6Ej(6NJ9jT z!~``TlTh>pFEq^OjX4j7T)odnab09OeDyanE-n`BrWlY(2e-nx8F?`Lz1nrE4iinc zns=7XPLT!Eo-PZemYPZH>r>{wRAay>Cv{GFxu<%2t9J_845hf2DDU{tc=^O1(+};4 zA)N=p^2UXt8h(~*qwA!R%pm#fOlnQ^xpaMpnMmyMNR2`NsMakXg)HF?2$?kVPMq#A zB@B^->Eb&rtifkI?&mh(MW~gvqEFwN$e)&;+H;+K7u#`US6;eUPByxa){Gs)Jl18b zcjBWeGV)m}lk#?Q1T@^l$xma}&te9T9^ztvb-}o`ln*G6k6o`RvIrBLXz=1r&Z@Pk z6|ih+S2JZDRi*54=2k4GaYwX4@np?xkG5(LvV_z4Oy z@;l+Qd14~igF73pB+EFVg`LZ3Y#(jcJ5u}DaHsJnFZ81M#TR--LN&0KuJ&b*%c2F( z$>R?Jcr%j48VU!QyFD_i*dtwyNJSvIji)F~A}z!6axe}0#fi2# zjak&ub!`yvS=0VNf!Yy*Cz(Ih&@PT<+++JbS(U(%D+Yem#+Eb>BxI*VCgwR`oxi(8 z%$&s*A{fg^Fe)-FX>CRgf<98mm4d!YNBeN0MWU`Ugc>M3yZdp@yb-PS!X+7tW+&$? z$#eltHeS51>0||ylv&9Huj{)&p;mxnq%rVa+&pC+=9j{uWjVG(^CZSU{&U)TV~=XE+uzM%SIhR>9$haIsd zB|+A>-={ZDAmo^oID@u-0&KIU1!!YR&uIRbz)-bO>lyQ1V|LTJ9wM*s%RE(Qu6gp} z4ICb3izQd54jV0u|=?s806E zco|JT=Vz}r80{u|`&pBz9y1^y)e=J{R! z@hji(aHtLne2a{8m1@VzSQ z58rRy|E_uZtDwKjQU4KC@t(5&Bk2ERtp6(L@3M%01S!5(qW_+g|Hvu+Rm9) 'string', + 'default' => 'https://bsky.social', + 'sanitize_callback' => function($input) { + // Force the default if empty + if (empty($input)) { + return 'https://bsky.social'; + } + // Ensure https:// is present + if (strpos($input, 'https://') !== 0) { + $input = 'https://' . $input; + } + // Remove any trailing slashes + return rtrim($input, '/'); + } + )); + register_setting('bluesky_connector_settings', 'bluesky_identifier', array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field' + )); + + register_setting('bluesky_connector_settings', 'bluesky_password', array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field' + )); + + register_setting('bluesky_connector_settings', 'bluesky_post_format', array( + 'type' => 'string', + 'default' => 'title-excerpt-image-link', + 'sanitize_callback' => 'sanitize_text_field' + )); + register_setting('bluesky_connector_settings', 'bluesky_include_title', array( + 'type' => 'boolean', + 'default' => true + )); + register_setting('bluesky_connector_settings', 'bluesky_title_separator', array( + 'type' => 'string', + 'default' => "\n\n", + 'sanitize_callback' => 'sanitize_text_field' + )); + + // Add settings sections and fields + add_settings_section( + 'bluesky_connector_main', + __('Bluesky Connection Settings', 'bluesky-connector'), + array($this, 'render_settings_section'), + 'bluesky_connector_settings' + ); + + add_settings_section( + 'bluesky_format_section', + __('Post Format Settings', 'bluesky-connector'), + array($this, 'render_format_section'), + 'bluesky_connector_settings' + ); + + add_settings_field( + 'bluesky_domain', + __('Bluesky Domain', 'bluesky-connector'), + array($this, 'render_domain_field'), + 'bluesky_connector_settings', + 'bluesky_connector_main' + ); + + add_settings_field( + 'bluesky_identifier', + __('Bluesky Handle', 'bluesky-connector'), + array($this, 'render_identifier_field'), + 'bluesky_connector_settings', + 'bluesky_connector_main' + ); + + add_settings_field( + 'bluesky_password', + __('App Password', 'bluesky-connector'), + array($this, 'render_password_field'), + 'bluesky_connector_settings', + 'bluesky_connector_main' + ); + + add_settings_field( + 'bluesky_post_format', + __('Post Layout', 'bluesky-connector'), + array($this, 'render_post_format_field'), + 'bluesky_connector_settings', + 'bluesky_format_section' + ); + + add_settings_field( + 'bluesky_include_title', + __('Include Post Title', 'bluesky-connector'), + array($this, 'render_include_title_field'), + 'bluesky_connector_settings', + 'bluesky_format_section' + ); + + add_settings_field( + 'bluesky_title_separator', + __('Title Separator', 'bluesky-connector'), + array($this, 'render_title_separator_field'), + 'bluesky_connector_settings', + 'bluesky_format_section' + ); + } + + + + + /** + * Add admin menu + */ + public function add_admin_menu() + { + add_options_page( + __('Bluesky Connector Settings', 'bluesky-connector'), + __('Bluesky Connector', 'bluesky-connector'), + 'manage_options', + 'bluesky-connector', + array($this, 'render_settings_page') + ); + } + + /** + * Render settings page + */ + public function render_settings_page() + { + if (!current_user_can('manage_options')) { + return; + } + + // Add process queue button + if (isset($_POST['process_queue_now']) && check_admin_referer('bluesky_process_queue')) { + $this->process_post_queue(); + add_settings_error( + 'bluesky_connector_settings', + 'queue_processed', + __('Queue processed.', 'bluesky-connector'), + 'success' + ); + } + + // Check for form submission + if (isset($_POST['action']) && $_POST['action'] === 'update_bluesky_settings') { + check_admin_referer('bluesky_connector_settings'); + $this->handle_settings_update(); + } + + // Get current settings + $settings = array( + 'domain' => get_option('bluesky_domain', 'https://bsky.social'), + 'identifier' => get_option('bluesky_identifier', ''), + 'connection_status' => get_option('bluesky_connection_status', ''), + 'last_error' => get_option('bluesky_last_error', '') + ); + + include BLUESKY_CONNECTOR_DIR . 'templates/settings-page.php'; + } + + /** + * Render settings section description + */ + public function render_settings_section() + { + echo '

' . esc_html__('Configure your Bluesky connection settings below.', 'bluesky-connector') . '

'; + } + + /** + * Render domain field + */ + public function render_domain_field() + { + $value = get_option('bluesky_domain', 'https://bsky.social'); + echo ''; + echo '

' . esc_html__('The Bluesky API domain (default: https://bsky.social)', 'bluesky-connector') . '

'; + } + + /** + * Render identifier field + */ + public function render_identifier_field() + { + $value = get_option('bluesky_identifier', ''); + echo ''; + echo '

' . esc_html__('Your Bluesky handle (e.g., username.bsky.social)', 'bluesky-connector') . '

'; + } + + /** + * Render password field + */ + public function render_password_field() + { + echo ''; + echo '

' . esc_html__('Your Bluesky app password (will not be stored)', 'bluesky-connector') . '

'; + } + + /** + * Handle settings update + */ + private function handle_settings_update() + { + $identifier = sanitize_text_field($_POST['bluesky_identifier']); + $password = sanitize_text_field($_POST['bluesky_password']); + $domain = esc_url_raw($_POST['bluesky_domain']); + + if (empty($identifier) || empty($password)) { + add_settings_error( + 'bluesky_connector_settings', + 'missing_credentials', + __('Both identifier and password are required.', 'bluesky-connector') + ); + return; + } + + // Try to authenticate + $auth = new Bluesky_Auth($identifier, $password); + $result = $auth->get_access_token(); + + if (isset($result['error'])) { + update_option('bluesky_connection_status', 'error'); + update_option('bluesky_last_error', $result['error']); + add_settings_error( + 'bluesky_connector_settings', + 'auth_failed', + sprintf(__('Authentication failed: %s', 'bluesky-connector'), $result['error']) + ); + } else { + update_option('bluesky_connection_status', 'connected'); + update_option('bluesky_last_error', ''); + add_settings_error( + 'bluesky_connector_settings', + 'settings_updated', + __('Settings saved and connected successfully.', 'bluesky-connector'), + 'success' + ); + } + } + + /** + * Handle post publish + */ + public function handle_post_publish($post_id, $post) + { + // Skip if not a public post + if ($post->post_status !== 'publish' || $post->post_type !== 'post') { + return; + } + + // Skip if already posted + if (get_post_meta($post_id, '_bluesky_posted', true)) { + return; + } + + // Add to queue + $queue = get_option('bluesky_post_queue', array()); + $queue[] = $post_id; + update_option('bluesky_post_queue', array_unique($queue)); + } + + /** + * Process post queue + */ + public function process_post_queue() + { + $queue = get_option('bluesky_post_queue', array()); + if (empty($queue)) { + return; + } + + $access_token = get_option('bluesky_access_jwt'); + $did = get_option('bluesky_did'); + + if (!$access_token || !$did) { + error_log('Bluesky Connector: Missing access token or DID'); + return; + } + + foreach ($queue as $key => $post_id) { + $post = get_post($post_id); + if (!$post) { + unset($queue[$key]); + continue; + } + + try { + $formatter = new Post_Formatter($access_token, $did); + $response = $formatter->format_and_post($post); + + if (!isset($response['error'])) { + unset($queue[$key]); + update_post_meta($post_id, '_bluesky_post_id', $response['uri']); + update_post_meta($post_id, '_bluesky_posted', current_time('mysql')); + update_post_meta($post_id, '_bluesky_status', 'success'); + } else { + update_post_meta($post_id, '_bluesky_status', 'error'); + update_post_meta($post_id, '_bluesky_error', $response['error']); + error_log('Bluesky Post Error: ' . print_r($response['error'], true)); + } + } catch (Exception $e) { + error_log('Bluesky Connector Exception: ' . $e->getMessage()); + update_post_meta($post_id, '_bluesky_status', 'error'); + update_post_meta($post_id, '_bluesky_error', $e->getMessage()); + } + } + + update_option('bluesky_post_queue', array_values($queue)); + } + + /** + * Add post meta box + */ + public function add_post_meta_box() + { + add_meta_box( + 'bluesky_post_status', + __('Bluesky Status', 'bluesky-connector'), + array($this, 'render_post_meta_box'), + 'post', + 'side', + 'default' + ); + } + + /** + * Render post meta box + */ + public function render_post_meta_box($post) + { + $status = get_post_meta($post->ID, '_bluesky_status', true); + $posted_date = get_post_meta($post->ID, '_bluesky_posted', true); + $error = get_post_meta($post->ID, '_bluesky_error', true); + $post_id = get_post_meta($post->ID, '_bluesky_post_id', true); + + include BLUESKY_CONNECTOR_DIR . 'templates/post-meta-box.php'; + } + + /** + * Save post meta box + */ + public function save_post_meta_box($post_id) + { + if (!current_user_can('edit_post', $post_id)) { + return; + } + + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + return; + } + + // Add any custom meta box saving logic here + } + + /** + * Handle retry post AJAX request + */ + public function handle_retry_post() + { + check_ajax_referer('bluesky_admin', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(['message' => __('Permission denied.', 'bluesky-connector')]); + } + + $post_id = intval($_POST['post_id']); + $post = get_post($post_id); + + if (!$post) { + wp_send_json_error(['message' => __('Post not found.', 'bluesky-connector')]); + } + + // Add to queue + $queue = get_option('bluesky_post_queue', array()); + $queue[] = $post_id; + update_option('bluesky_post_queue', array_unique($queue)); + + // Update post meta + update_post_meta($post_id, '_bluesky_status', 'queued'); + delete_post_meta($post_id, '_bluesky_error'); + + wp_send_json_success(['message' => __('Post queued for retry.', 'bluesky-connector')]); + } + + /** + * Handle share post AJAX request + */ + public function handle_share_post() + { + check_ajax_referer('bluesky_admin', 'nonce'); + + if (!current_user_can('edit_posts')) { + wp_send_json_error(['message' => __('Permission denied.', 'bluesky-connector')]); + } + + $post_id = intval($_POST['post_id']); + $post = get_post($post_id); + + if (!$post) { + wp_send_json_error(['message' => __('Post not found.', 'bluesky-connector')]); + } + + // Add to queue + $queue = get_option('bluesky_post_queue', array()); + $queue[] = $post_id; + update_option('bluesky_post_queue', array_unique($queue)); + + // Update post meta + update_post_meta($post_id, '_bluesky_status', 'queued'); + + wp_send_json_success(['message' => __('Post queued for sharing.', 'bluesky-connector')]); + } + + /** + * Enqueue admin scripts and styles + */ + public function enqueue_admin_scripts($hook) + { + if ('post.php' !== $hook && 'post-new.php' !== $hook) { + return; + } + + wp_enqueue_script( + 'bluesky-admin', + BLUESKY_CONNECTOR_URL . 'assets/js/admin.js', + array('jquery'), + BLUESKY_CONNECTOR_VERSION, + true + ); + + wp_localize_script('bluesky-admin', 'blueskyAdmin', array( + 'nonce' => wp_create_nonce('bluesky_admin'), + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'strings' => array( + 'error' => __('An error occurred. Please try again.', 'bluesky-connector'), + 'success' => __('Operation completed successfully.', 'bluesky-connector') + ) + )); + + wp_enqueue_style( + 'bluesky-admin', + BLUESKY_CONNECTOR_URL . 'assets/css/admin.css', + array(), + BLUESKY_CONNECTOR_VERSION + ); + } + + /** + * Plugin activation + */ + public function activate() + { + // Create necessary options with default values + add_option('bluesky_domain', 'https://bsky.social'); + add_option('bluesky_post_queue', array()); + add_option('bluesky_connection_status', ''); + + // Schedule cron job + if (!wp_next_scheduled('process_bluesky_post_queue')) { + wp_schedule_event(time(), 'hourly', 'process_bluesky_post_queue'); + } + + // Create custom capabilities + $role = get_role('administrator'); + if ($role) { + $role->add_cap('manage_bluesky_settings'); + } + + // Flush rewrite rules + flush_rewrite_rules(); + } + + /** + * Plugin deactivation + */ + public function deactivate() + { + // Clear scheduled events + wp_clear_scheduled_hook('process_bluesky_post_queue'); + + // Remove custom capabilities + $role = get_role('administrator'); + if ($role) { + $role->remove_cap('manage_bluesky_settings'); + } + } + + /** + * Display admin notices + */ + public function admin_notices() + { + if (!current_user_can('manage_options')) { + return; + } + + $screen = get_current_screen(); + if ($screen->id !== 'settings_page_bluesky-connector') { + return; + } + + settings_errors('bluesky_connector_settings'); + } + + /** + * Add settings link to plugins page + */ + public function add_settings_link($links) + { + $settings_link = sprintf( + '%s', + admin_url('options-general.php?page=bluesky-connector'), + __('Settings', 'bluesky-connector') + ); + array_unshift($links, $settings_link); + return $links; + } + + /** + * Log plugin errors + */ + private function log_error($message, $data = array()) + { + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log(sprintf( + '[Bluesky Connector] %s | Data: %s', + $message, + print_r($data, true) + )); + } + } + + /** + * Filter for customizing post content before sending to Bluesky + */ + public function filter_post_content($content, $post) + { + return apply_filters('bluesky_post_content', $content, $post); + } + + /** + * Check if post should be shared to Bluesky + */ + private function should_share_post($post) + { + // Skip if post is not published + if ($post->post_status !== 'publish') { + return false; + } + + // Skip if post type is not supported + $supported_post_types = apply_filters('bluesky_supported_post_types', array('post')); + if (!in_array($post->post_type, $supported_post_types)) { + return false; + } + + // Skip if already shared + if (get_post_meta($post->ID, '_bluesky_posted', true)) { + return false; + } + + // Allow custom filtering + return apply_filters('bluesky_should_share_post', true, $post); + } + + public function render_format_section() { + echo '

' . esc_html__('Customize how your posts appear on Bluesky.', 'bluesky-connector') . '

'; + } + + public function render_post_format_field() { + $format = get_option('bluesky_post_format', 'title-excerpt-image-link'); + $options = array( + 'title-excerpt-image-link' => __('Title → Excerpt → Image → Link', 'bluesky-connector'), + 'title-image-excerpt-link' => __('Title → Image → Excerpt → Link', 'bluesky-connector'), + 'image-title-excerpt-link' => __('Image → Title → Excerpt → Link', 'bluesky-connector'), + 'excerpt-image-link' => __('Excerpt → Image → Link', 'bluesky-connector'), + 'title-excerpt-link' => __('Title → Excerpt → Link (No Image)', 'bluesky-connector'), + ); + + echo ''; + echo '

' . esc_html__('Choose how you want your posts to be formatted on Bluesky.', 'bluesky-connector') . '

'; + } + + public function render_include_title_field() { + $include_title = get_option('bluesky_include_title', true); + printf( + '', + checked($include_title, true, false), + __('Include post title at the beginning', 'bluesky-connector') + ); + echo '

' . esc_html__('Add the post title to the beginning of each Bluesky post.', 'bluesky-connector') . '

'; + } + + public function render_title_separator_field() { + $separator = get_option('bluesky_title_separator', "\n\n"); + $options = array( + "\n\n" => __('Double Line Break', 'bluesky-connector'), + "\n" => __('Single Line Break', 'bluesky-connector'), + " - " => __('Dash', 'bluesky-connector'), + ": " => __('Colon', 'bluesky-connector'), + ); + + echo ''; + echo '

' . esc_html__('Choose how to separate the title from the excerpt.', 'bluesky-connector') . '

'; + } +} + +/** + * Initialize the plugin + */ +function bluesky_connector_init() +{ + return Bluesky_Connector::get_instance(); +} + +// Initialize the plugin +add_action('plugins_loaded', 'bluesky_connector_init'); + +/** + * Register uninstall hook + */ +register_uninstall_hook(__FILE__, 'bluesky_connector_uninstall'); + +/** + * Clean up plugin data on uninstall + */ +function bluesky_connector_uninstall() +{ + // Only run if explicitly uninstalling + if (!defined('WP_UNINSTALL_PLUGIN')) { + return; + } + + // Remove options + delete_option('bluesky_domain'); + delete_option('bluesky_identifier'); + delete_option('bluesky_access_jwt'); + delete_option('bluesky_refresh_jwt'); + delete_option('bluesky_did'); + delete_option('bluesky_post_queue'); + delete_option('bluesky_connection_status'); + delete_option('bluesky_last_error'); + + // Remove capabilities + $role = get_role('administrator'); + if ($role) { + $role->remove_cap('manage_bluesky_settings'); + } + + // Clear scheduled hooks + wp_clear_scheduled_hook('process_bluesky_post_queue'); + + // Remove post meta + global $wpdb; + $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_bluesky_%'"); +} \ No newline at end of file diff --git a/includes/bluesky-api.php b/includes/bluesky-api.php new file mode 100644 index 0000000..213e096 --- /dev/null +++ b/includes/bluesky-api.php @@ -0,0 +1,70 @@ +api_key = $api_key; + $this->did = $did; + } + + public function create_post($post_data) { + $headers = array( + 'Authorization' => 'Bearer ' . $this->api_key, + 'Content-Type' => 'application/json', + ); + + $response = wp_remote_post($this->api_url . '/com.atproto.repo.createRecord', array( + 'headers' => $headers, + 'body' => json_encode(array( + 'repo' => $this->did, + 'collection' => 'app.bsky.feed.post', + 'record' => $post_data, + )), + )); + + if (is_wp_error($response)) { + return array('error' => $response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } + + public function resolve_handle($handle) { + $response = wp_remote_get($this->api_url . '/com.atproto.identity.resolveHandle', array( + 'query' => array( + 'handle' => $handle, + ), + )); + + if (is_wp_error($response)) { + return array('error' => $response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } + + public function upload_blob($file_path, $mime_type) { + $headers = array( + 'Authorization' => 'Bearer ' . $this->api_key, + 'Content-Type' => $mime_type, + ); + + $file_contents = file_get_contents($file_path); + if ($file_contents === false) { + return array('error' => 'Failed to read file.'); + } + + $response = wp_remote_post($this->api_url . '/com.atproto.repo.uploadBlob', array( + 'headers' => $headers, + 'body' => $file_contents, + )); + + if (is_wp_error($response)) { + return array('error' => $response->get_error_message()); + } + + return json_decode(wp_remote_retrieve_body($response), true); + } +} \ No newline at end of file diff --git a/includes/bluesky-auth.php b/includes/bluesky-auth.php new file mode 100644 index 0000000..11e0d16 --- /dev/null +++ b/includes/bluesky-auth.php @@ -0,0 +1,152 @@ +identifier = $identifier; + $this->password = $password; + + // Get domain with fallback and force https:// + $domain = get_option('bluesky_domain', 'https://bsky.social'); + if (empty($domain)) { + $domain = 'https://bsky.social'; + } + if (strpos($domain, 'https://') !== 0) { + $domain = 'https://' . $domain; + } + + // Ensure proper URL format + $this->api_domain = rtrim($domain, '/') . '/'; + + // Debug logs + error_log('Bluesky Auth - Using domain setting: ' . get_option('bluesky_domain')); + error_log('Bluesky Auth - Processed domain: ' . $this->api_domain); + error_log('Bluesky Auth - Identifier: ' . $this->identifier); + } + + public function get_access_token() { + if ($this->should_refresh_token()) { + return $this->refresh_access_token(); + } + + $token_url = $this->api_domain . 'xrpc/com.atproto.server.createSession'; + + // Debug log + error_log('Bluesky Auth - Attempting connection to: ' . $token_url); + + $headers = array( + 'Content-Type' => 'application/json', + ); + + $body = json_encode(array( + 'identifier' => $this->identifier, + 'password' => $this->password, + )); + + // Debug log + error_log('Bluesky Auth - Request body: ' . $body); + + $wp_version = get_bloginfo('version'); + $user_agent = apply_filters('http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo('url')); + + $response = wp_remote_post($token_url, array( + 'headers' => $headers, + 'user-agent' => "$user_agent; Bluesky Connector", + 'body' => $body, + 'timeout' => 30, // Increase timeout + )); + + if (is_wp_error($response)) { + $error_message = $response->get_error_message(); + error_log('Bluesky Auth Error: ' . $error_message); + return array('error' => $error_message); + } + + // Debug response + $status_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + error_log('Bluesky Auth - Response status: ' . $status_code); + error_log('Bluesky Auth - Response body: ' . $response_body); + + $data = json_decode($response_body, true); + + if (!empty($data['accessJwt']) && !empty($data['refreshJwt']) && !empty($data['did'])) { + update_option('bluesky_access_jwt', sanitize_text_field($data['accessJwt'])); + update_option('bluesky_refresh_jwt', sanitize_text_field($data['refreshJwt'])); + update_option('bluesky_did', sanitize_text_field($data['did'])); + update_option('bluesky_token_created', time()); + delete_option('bluesky_password'); // Don't store password + return $data['accessJwt']; + } + + // More detailed error reporting + $error_message = isset($data['error']) ? $data['error'] : 'Failed to get access token'; + if (isset($data['message'])) { + $error_message .= ' - ' . $data['message']; + } + error_log('Bluesky Auth - Error: ' . $error_message); + return array('error' => $error_message); + } + + private function should_refresh_token() { + $token_created = get_option('bluesky_token_created'); + $refresh_token = get_option('bluesky_refresh_jwt'); + + // Refresh if token is older than 23 hours or doesn't exist + return !empty($refresh_token) && ($token_created < (time() - 82800)); + } + + private function refresh_access_token() { + $refresh_token = get_option('bluesky_refresh_jwt'); + if (empty($refresh_token)) { + return array('error' => 'No refresh token available'); + } + + $refresh_url = $this->api_domain . 'xrpc/com.atproto.server.refreshSession'; + + // Debug log + error_log('Bluesky Auth - Attempting token refresh at: ' . $refresh_url); + + $wp_version = get_bloginfo('version'); + $user_agent = apply_filters('http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo('url')); + + $response = wp_remote_post($refresh_url, array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $refresh_token, + 'Content-Type' => 'application/json', + ), + 'user-agent' => "$user_agent; Bluesky Connector", + 'timeout' => 30, + )); + + if (is_wp_error($response)) { + $error_message = $response->get_error_message(); + error_log('Bluesky Token Refresh Error: ' . $error_message); + return array('error' => $error_message); + } + + // Debug response + $status_code = wp_remote_retrieve_response_code($response); + $response_body = wp_remote_retrieve_body($response); + error_log('Bluesky Auth Refresh - Response status: ' . $status_code); + error_log('Bluesky Auth Refresh - Response body: ' . $response_body); + + $data = json_decode($response_body, true); + + if (!empty($data['accessJwt']) && !empty($data['refreshJwt'])) { + update_option('bluesky_access_jwt', sanitize_text_field($data['accessJwt'])); + update_option('bluesky_refresh_jwt', sanitize_text_field($data['refreshJwt'])); + update_option('bluesky_token_created', time()); + return $data['accessJwt']; + } + + $error_message = isset($data['error']) ? $data['error'] : 'Failed to refresh token'; + if (isset($data['message'])) { + $error_message .= ' - ' . $data['message']; + } + error_log('Bluesky Auth Refresh - Error: ' . $error_message); + return array('error' => $error_message); + } +} \ No newline at end of file diff --git a/includes/post-formatter.php b/includes/post-formatter.php new file mode 100644 index 0000000..5ba409a --- /dev/null +++ b/includes/post-formatter.php @@ -0,0 +1,243 @@ +api = new Bluesky_API($access_token, $did); + } + + public function format_and_post($post) { + $content = $this->get_formatted_content($post); + + $post_data = array( + '$type' => 'app.bsky.feed.post', + 'text' => $content, + 'createdAt' => gmdate('c', strtotime($post->post_date_gmt)), + 'langs' => array('en') + ); + + // Add facets for the URL + $post_data['facets'] = $this->parse_facets($post); + + // Handle image embed separately from text content + if (has_post_thumbnail($post->ID)) { + $image_data = $this->handle_featured_image($post->ID); + if (!empty($image_data) && !isset($image_data['error'])) { + $post_data['embed'] = $image_data; + } + } + + return $this->api->create_post($post_data); + } + + private function get_formatted_content($post) { + $format = get_option('bluesky_post_format', 'image-title-excerpt-link'); + $include_title = get_option('bluesky_include_title', true); + + // Get individual components + $title = $include_title ? $post->post_title : ''; + $excerpt = $this->get_excerpt($post); + $url = wp_get_shortlink($post->ID); + + // Start building content with explicit line breaks + $content = ''; + + // Add title with line break if it exists + if (!empty($title)) { + $content .= $title . "\n\n"; + } + + // Add excerpt if it exists + if (!empty($excerpt)) { + $content .= $excerpt . "\n\n"; // Add double line break after excerpt + } + + // Add URL on its own line + if (!empty($url)) { + $content .= $url; // URL starts on new line due to previous \n\n + } + + return $content; + } + + private function get_excerpt($post) { + $text = wp_strip_all_tags($post->post_excerpt); + if (empty($text)) { + $text = wp_strip_all_tags($post->post_content); + } + + $url = wp_get_shortlink($post->ID); + $include_title = get_option('bluesky_include_title', true); + + // Calculate available length accounting for spacing and new lines + $available_length = $this->max_length; + $available_length -= strlen($url); + $available_length -= 2; // Account for \n\n after excerpt + + if ($include_title) { + $available_length -= strlen($post->post_title); + $available_length -= 2; // Account for \n\n after title + } + + if (mb_strlen($text) > $available_length) { + $text = mb_substr($text, 0, $available_length - 3) . '...'; + } + + return $text; + } + + private function parse_facets($post) { + $facets = array(); + $content = $this->get_formatted_content($post); + + // Add link facet for the post URL + $url = wp_get_shortlink($post->ID); + $text_bytes = mb_convert_encoding($content, 'UTF-8'); + $url_position = mb_strrpos($text_bytes, $url); + + if ($url_position !== false) { + $facets[] = array( + 'index' => array( + 'byteStart' => $url_position, + 'byteEnd' => $url_position + strlen($url), + ), + 'features' => array( + array( + '$type' => 'app.bsky.richtext.facet#link', + 'uri' => $url, + ), + ), + ); + } + + return $facets; + } + + private function handle_featured_image($post_id) { + $image_id = get_post_thumbnail_id($post_id); + $image_path = get_attached_file($image_id); + + if (!$image_path) { + return array('error' => 'Image file not found'); + } + + $mime_type = get_post_mime_type($image_id); + + // Get image data + $image_data = file_get_contents($image_path); + if ($image_data === false) { + return array('error' => 'Failed to read image file'); + } + + // Check file size (1MB limit for Bluesky) + if (strlen($image_data) > 1000000) { + // If image is too large, attempt to resize it + $resized = $this->resize_image($image_path); + if ($resized) { + $image_data = file_get_contents($resized); + unlink($resized); // Clean up temporary file + } else { + return array('error' => 'Image file size exceeds 1MB limit and resize failed'); + } + } + + // Upload image blob + $response = $this->api->upload_blob($image_path, $mime_type); + + if (isset($response['error'])) { + return $response; + } + + // Get the alt text + $alt_text = get_post_meta($image_id, '_wp_attachment_image_alt', true) ?: ''; + + return array( + '$type' => 'app.bsky.embed.images', + 'images' => array( + array( + 'alt' => $alt_text, + 'image' => $response['blob'], + ), + ), + ); + } + + private function resize_image($image_path) { + // Only proceed if GD is available + if (!function_exists('imagecreatefrompng')) { + return false; + } + + $mime_type = mime_content_type($image_path); + list($width, $height) = getimagesize($image_path); + + // Calculate new dimensions while maintaining aspect ratio + $max_dimension = 1000; // Reasonable size that should result in < 1MB file + if ($width > $height) { + $new_width = $max_dimension; + $new_height = floor($height * ($max_dimension / $width)); + } else { + $new_height = $max_dimension; + $new_width = floor($width * ($max_dimension / $height)); + } + + // Create new image + $new_image = imagecreatetruecolor($new_width, $new_height); + + // Handle different image types + switch ($mime_type) { + case 'image/jpeg': + $source = imagecreatefromjpeg($image_path); + break; + case 'image/png': + $source = imagecreatefrompng($image_path); + // Preserve transparency + imagealphablending($new_image, false); + imagesavealpha($new_image, true); + break; + case 'image/gif': + $source = imagecreatefromgif($image_path); + break; + default: + return false; + } + + if (!$source) { + return false; + } + + // Resize + imagecopyresampled( + $new_image, + $source, + 0, 0, 0, 0, + $new_width, + $new_height, + $width, + $height + ); + + // Create temporary file + $temp_file = tempnam(sys_get_temp_dir(), 'bluesky_img_'); + + // Save resized image + switch ($mime_type) { + case 'image/jpeg': + imagejpeg($new_image, $temp_file, 85); + break; + case 'image/png': + imagepng($new_image, $temp_file, 8); + break; + case 'image/gif': + imagegif($new_image, $temp_file); + break; + } + + // Clean up + imagedestroy($source); + imagedestroy($new_image); + + return $temp_file; + } +} \ No newline at end of file diff --git a/includes/settings.php b/includes/settings.php new file mode 100644 index 0000000..6064e31 --- /dev/null +++ b/includes/settings.php @@ -0,0 +1,2 @@ + + + + +

+ + ' . esc_html__('Posted', 'bluesky-connector') . ''; + break; + case 'error': + echo '' . esc_html__('Error', 'bluesky-connector') . ''; + break; + case 'queued': + echo '' . esc_html__('Queued', 'bluesky-connector') . ''; + break; + default: + echo '' . esc_html__('Unknown', 'bluesky-connector') . ''; + } + ?> +

+ + +

+ + +

+ + + +

+ + +

+ + + +

+ + +

+ + +
+ + + +
+ +

+ + + \ No newline at end of file diff --git a/templates/settings-page.php b/templates/settings-page.php new file mode 100644 index 0000000..de98032 --- /dev/null +++ b/templates/settings-page.php @@ -0,0 +1,167 @@ +
+

+ + + +
+

+
+ +
+

+
+ + + +
+

+
+ + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+
+ + + + + + + + + + + + +
+
+ +
+

+ +

+ +

+ + 0) : ?> +
+ + +
+ +
+ + +
+

+ + + + + + + + + + +
+ + +
\ No newline at end of file