···11import { html } from "hono/html";
22-import { BSKY_MAX_APP_PASSWORD_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
33-import UsernameField from "./usernameField";
22+import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
33+import { UsernameField } from "./usernameField";
44+import { BSkyAppPasswordField, DashboardPasswordField } from "./passwordFields";
55+import { PWAutoCompleteSettings } from "../types.d";
4657export function Settings() {
68 return (
···22242325 <label>
2426 Dashboard Pass:
2525- <input type="password" name="password" minlength={MIN_DASHBOARD_PASS} maxlength={MAX_DASHBOARD_PASS} />
2727+ <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} />
2628 <small>The password to access this website</small>
2729 </label>
2830 <label>
2931 BSky App Password:
3030- <input type="password" name="bskyAppPassword" maxlength={BSKY_MAX_APP_PASSWORD_LENGTH} />
3131- <small>If you need to change your application password for whatever reason</small>
3232+ <BSkyAppPasswordField />
3333+ <small>If you need to change your application password, you can <a href="https://bsky.app/settings/app-passwords" target="_blank">get a new one here</a></small>
3234 </label>
3335 <label>
3436 BSky PDS:
···3638 <small>If you have not changed your PDS (or do not know what that means), you should leave this blank!</small>
3739 </label>
3840 </form>
3939- <br />
4041 <progress id="spinner" class="htmx-indicator" />
4141- <center>
4242- <div id="accountResponse">
4343- </div>
4444- </center>
4242+ <div id="accountResponse">
4343+ </div>
4544 </section>
4645 <footer>
4746 <button id="deleteAccountButton" class="btn-error" style="float: left;">Delete</button>
···6362 <small>The password to access this website</small>
6463 </label>
6564 </form>
6666- <br />
6765 <progress id="delSpinner" class="htmx-indicator" />
6868- <center>
6966 <div id="accountDelete">
7067 </div>
7171- </center>
7268 <footer>
7369 <button class="btn-error" form="delAccountForm">Delete</button>
7470 <button class="secondary" onclick='closeDeleteModal();'>Cancel</button>
+3-2
src/layout/usernameField.tsx
···88 required?: boolean;
99};
10101111-export default function UsernameField(props?: UsernameFieldProps) {
1212- const hintText = props?.hintText ? raw(props.hintText) : raw("This is your Bluesky username, in the format of a custom domain or like <code>USERNAME.bsky.social</code>.");
1111+export function UsernameField(props?: UsernameFieldProps) {
1212+ const hintText = props?.hintText ? raw(props.hintText) :
1313+ raw("This is your Bluesky username, in the format of a custom domain or like <code>USERNAME.bsky.social</code>.");
1314 // default required true.
1415 const inputRequired = (props) ? (props?.required || false) : true;
1516 return (
+29
src/layout/violationsBar.tsx
···11+import { Context } from "hono";
22+import { getViolationsForCurrentUser } from "../utils/dbQuery";
33+import { Violation } from "../types.d";
44+55+export async function ViolationNoticeBar(props: any) {
66+ const ctx:Context = props.ctx;
77+ const {success, results} = await getViolationsForCurrentUser(ctx);
88+ if (success && results.length > 0) {
99+ let errorStr = "";
1010+ const violationData:Violation = (results[0] as Violation)
1111+ if (violationData.tosViolation) {
1212+ errorStr = "Your account is in violation of SkyScheduler usage.";
1313+ } else if(violationData.userPassInvalid) {
1414+ errorStr = "Your Bluesky handle or application password is invalid. Please update these in the settings.";
1515+ } else if (violationData.accountSuspended) {
1616+ errorStr = "Your account has been suspended by Bluesky. Some features may not work at this time";
1717+ } else if (violationData.accountGone) {
1818+ errorStr = "Unable to find your account, update your Bluesky handle in the settings";
1919+ }
2020+ return (
2121+ <div class="warning-box">
2222+ <span class="warning"><b>WARNING</b>: Account error found! {errorStr}</span>
2323+ </div>
2424+ );
2525+ }
2626+ return (
2727+ <></>
2828+ );
2929+};
···22import { BaseLayout } from "../layout/main";
33import NavTags from "../layout/navTags";
44import AccountHandler from "../layout/account";
55-import UsernameField from "../layout/usernameField";
55+import { UsernameField } from "../layout/usernameField";
66import TurnstileCaptcha from "../layout/turnstile";
7788export default function ForgotPassword(props:any) {
+4-2
src/pages/login.tsx
···11import { BaseLayout } from "../layout/main";
22import NavTags from "../layout/navTags";
33import AccountHandler from "../layout/account";
44-import UsernameField from "../layout/usernameField";
44+import { UsernameField } from "../layout/usernameField";
55+import { DashboardPasswordField } from "../layout/passwordFields";
66+import { PWAutoCompleteSettings } from "../types.d";
5768export default function Login() {
79 const links = [{title: "Sign Up", url: "/signup"}, {title: "Forgot Password", url: "/forgot"}];
···18201921 <label>
2022 Dashboard Password
2121- <input type="password" name="password" id="password" required />
2323+ <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} required={true} />
2224 <small><b>NOTE</b>: This password is not related to your bluesky account!</small>
2325 </label>
2426 </AccountHandler>
+6-6
src/pages/signup.tsx
···11import { Context } from "hono";
22import { BaseLayout } from "../layout/main";
33import { isUsingInviteKeys } from "../utils/inviteKeys";
44-import { BSKY_MAX_APP_PASSWORD_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
44+import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
55import NavTags from "../layout/navTags";
66import isEmpty from "just-is-empty";
77import AccountHandler from "../layout/account";
88-import UsernameField from "../layout/usernameField";
88+import { UsernameField } from "../layout/usernameField";
99import TurnstileCaptcha from "../layout/turnstile";
1010import FooterCopyright from "../layout/footer";
1111+import { BSkyAppPasswordField, DashboardPasswordField } from "../layout/passwordFields";
1212+import { PWAutoCompleteSettings } from "../types.d";
11131214export default function Signup(props:any) {
1315 const ctx: Context = props.c;
···30323133 <label>
3234 Dashboard Password
3333- <input type="password" name="password" minlength={MIN_DASHBOARD_PASS} maxlength={MAX_DASHBOARD_PASS} required
3434- autocomplete="new-password" />
3535+ <DashboardPasswordField autocomplete={PWAutoCompleteSettings.NewPass} required={true} />
3536 <small>Create a new password to use to login to this website. Passwords should be {MIN_DASHBOARD_PASS} to {MAX_DASHBOARD_PASS} characters long.</small>
3637 </label>
37383839 <label>
3940 Bluesky App Password
4040- <input type="password" name="bskyAppPassword" maxlength={BSKY_MAX_APP_PASSWORD_LENGTH} placeholder="" required
4141- data-1p-ignore data-bwignore data-lpignore="true" data-protonpass-ignore="true" autocomplete="off" />
4141+ <BSkyAppPasswordField required={true} />
4242 <small>
4343 If you need a bluesky app password for your account, <a target="_blank" href="https://bsky.app/settings/app-passwords">you can get one here</a>.
4444 </small>
+27-13
src/utils/bskyApi.ts
···11import { type AppBskyFeedPost, AtpAgent, RichText } from '@atproto/api';
22-import { Bindings, Post, Repost, PostLabel, EmbedData, PostResponseObject, LooseObj } from '../types.d';
22+import { Bindings, Post, Repost, PostLabel, EmbedData, PostResponseObject, LooseObj, PlatformLoginResponse } from '../types.d';
33import { MAX_ALT_TEXT, MAX_EMBEDS, MAX_LENGTH, MAX_POSTED_LENGTH } from '../limits.d';
44-import { updatePostData, getBskyUserPassForId } from './dbQuery';
44+import { updatePostData, getBskyUserPassForId, createViolationForUser } from './dbQuery';
55import { deleteEmbedsFromR2 } from './r2Query';
66import {imageDimensionsFromStream} from 'image-dimensions';
77import truncate from "just-truncate";
···2222 password: pass,
2323 });
2424 if (!loginResponse.success) {
2525- console.warn(`could not login as user ${user}`);
2626- return false;
2525+ if (loginResponse.data.active == false) {
2626+ switch (loginResponse.data.status) {
2727+ case "deactivated":
2828+ return PlatformLoginResponse.Deactivated;
2929+ case "suspended":
3030+ return PlatformLoginResponse.Suspended;
3131+ case "takendown":
3232+ return PlatformLoginResponse.TakenDown;
3333+ }
3434+ return PlatformLoginResponse.InvalidAccount;
3535+ }
3636+ return PlatformLoginResponse.PlatformOutage;
2737 }
2828- return true;
3838+ return PlatformLoginResponse.Ok;
2939 } catch (err) {
3040 console.error(`encountered exception on login for user ${user}, err ${err}`);
3141 }
3232- return false;
4242+ return PlatformLoginResponse.UnhandledError;
3343}
34443545export const makeRepost = async (env: Bindings, content: Repost) => {
···4555 return false;
4656 }
47574848- const loginResponse = await loginToBsky(agent, user, pass);
4949- if (!loginResponse) {
5050- // TODO: Probably should handle failure better here.
5858+ const loginResponse:PlatformLoginResponse = await loginToBsky(agent, user, pass);
5959+ if (loginResponse != PlatformLoginResponse.Ok) {
6060+ const addViolation:boolean = await createViolationForUser(env, content.userId, loginResponse);
6161+ if (addViolation)
6262+ console.error(`Unable to login to make repost from user ${content.userId} with violation ${loginResponse}`);
5163 return false;
5264 }
5365···8294 return null;
8395 }
84968585- const loginResponse = await loginToBsky(agent, user, pass);
8686- if (!loginResponse) {
8787- // TODO: Probably should handle failure better here.
9797+ const loginResponse:PlatformLoginResponse = await loginToBsky(agent, user, pass);
9898+ if (loginResponse != PlatformLoginResponse.Ok) {
9999+ const addViolation:boolean = await createViolationForUser(env, content.user, loginResponse);
100100+ if (addViolation)
101101+ console.error(`Unable to login to make post ${content.postid} with violation ${loginResponse}`);
88102 return null;
89103 }
90104···103117 const posts:PostResponseObject[] = [];
104118105119 const postSegment = async (data: string) => {
106106- let postRecord:AppBskyFeedPost.Record = {
120120+ let postRecord:AppBskyFeedPost.Record = {
107121 $type: 'app.bsky.feed.post',
108122 text: data,
109123 facets: rt.facets,
+2-2
src/utils/bskyMsg.ts
···11import { AtpAgent, RichText } from '@atproto/api';
22import { loginToBsky } from './bskyApi';
33-import { Bindings } from '../types';
33+import { Bindings, PlatformLoginResponse } from '../types.d';
4455export const createDMWithUser = async (env: Bindings, user: string, msg: string) => {
66 const agent = new AtpAgent({
···88 });
991010 const loginResponse = await loginToBsky(agent, env.RESET_BOT_USERNAME, env.RESET_BOT_APP_PASS);
1111- if (!loginResponse) {
1111+ if (loginResponse != PlatformLoginResponse.Ok) {
1212 console.error("Unable to login to the bot to send reset password messages");
1313 return false;
1414 }
+74-10
src/utils/dbQuery.ts
···11import { Context } from "hono";
22import { DrizzleD1Database, drizzle } from "drizzle-orm/d1";
33-import { sql, and, or, gt, eq, lte, inArray, desc, count, getTableColumns } from "drizzle-orm";
33+import { sql, and, gt, eq, lte, inArray, desc, count, getTableColumns, notInArray, ne } from "drizzle-orm";
44import { BatchItem } from "drizzle-orm/batch";
55-import { posts, reposts } from "../db/app.schema";
55+import { posts, reposts, violations } from "../db/app.schema";
66import { accounts, users } from "../db/auth.schema";
77import { PostSchema } from "../validation/postSchema";
88-import { Bindings } from "../types";
88+import { Bindings, LooseObj, PlatformLoginResponse } from "../types.d";
99import { MAX_POSTED_LENGTH } from "../limits.d";
1010import { createPostObject, floorCurrentTime, floorGivenTime } from "./helpers";
1111import { deleteEmbedsFromR2 } from "./r2Query";
···6767 if (userData) {
6868 const db: DrizzleD1Database = drizzle(c.env.DB);
6969 let queriesToExecute:BatchItem<"sqlite">[] = [];
7070+ const updatedPassword = has(newData, "password");
70717171- if (has(newData, "password")) {
7272+ if (updatedPassword) {
7273 // cache out the new hash
7374 const newPassword = newData.password;
7475 // remove it from the original object
···8081 .where(eq(accounts.userId, userData.id)));
8182 }
82838484+ // If we have new data about the username, pds, or password, then clear account invalid violations
8585+ if (updatedPassword || has(newData, "username") || has(newData, "pds")) {
8686+ queriesToExecute.push(getViolationDeleteQueryForUser(db, userData.id));
8787+ }
8888+8389 if (!isEmpty(newData)) {
8490 queriesToExecute.push(db.update(users).set(newData)
8591 .where(eq(users.id, userData.id)));
···136142 return { ok: false, msg: "Scheduled date must be in the future" };
137143 }
138144145145+ // TODO: prevent anything from happening if you are currently in violations table
146146+139147 const postUUID = uuidv4();
140148 let dbOperations:BatchItem<"sqlite">[] = [
141149 db.insert(posts).values({
···168176 const db: DrizzleD1Database = drizzle(env.DB);
169177 const currentTime: Date = floorCurrentTime();
170178179179+ const violationUsers = db.select({data: violations.userId}).from(violations);
171180 return await db.select().from(posts)
172172- .where(and(lte(posts.scheduledDate, currentTime),
173173- eq(posts.posted, false)))
174174- .all();
181181+ .where(and(and(
182182+ lte(posts.scheduledDate, currentTime), eq(posts.posted, false)),
183183+ notInArray(posts.userId, violationUsers))
184184+ ).all();
175185};
176186177187export const getAllRepostsForGivenTime = async (env: Bindings, givenDate: Date) => {
···179189 const db: DrizzleD1Database = drizzle(env.DB);
180190 const query = db.select({uuid: reposts.uuid}).from(reposts)
181191 .where(lte(reposts.scheduledDate, givenDate));
192192+ const violationsQuery = db.select({data: violations.userId}).from(violations);
182193 return await db.select({uri: posts.uri, cid: posts.cid, userId: posts.userId })
183194 .from(posts)
184184- .where(inArray(posts.uuid, query))
195195+ .where(and(inArray(posts.uuid, query), notInArray(posts.userId, violationsQuery)))
185196 .all();
186197};
187198···206217 return false;
207218208219 const db: DrizzleD1Database = drizzle(c.env.DB);
209209- const result = await db.update(posts).set(newData).where(and(eq(posts.uuid, id), eq(posts.userId, userData.id)));
210210- return result.success;
220220+ const {success} = await db.update(posts).set(newData).where(and(eq(posts.uuid, id), eq(posts.userId, userData.id)));
221221+ return success;
211222};
212223213224export const getPostById = async(c: Context, id: string) => {
···287298 postTruncation.forEach(async item => {
288299 await db.update(posts).set({ content: truncate(item.content, MAX_POSTED_LENGTH) }).where(eq(posts.uuid, item.id));
289300 });
301301+ }
302302+};
303303+304304+export const createViolationForUser = async(env: Bindings, userId: string, violationType: PlatformLoginResponse) => {
305305+ const NoHandleState:PlatformLoginResponse[] = [PlatformLoginResponse.Ok, PlatformLoginResponse.PlatformOutage, PlatformLoginResponse.None, PlatformLoginResponse.UnhandledError];
306306+ // Don't do anything in these cases
307307+ if (violationType in NoHandleState) {
308308+ console.warn(`createViolationForUser got a not valid add request for user ${userId} with violation ${violationType}`);
309309+ return false;
310310+ }
311311+312312+ const db: DrizzleD1Database = drizzle(env.DB);
313313+ let valuesUpdate:LooseObj = {};
314314+ switch (violationType)
315315+ {
316316+ case PlatformLoginResponse.InvalidAccount:
317317+ case PlatformLoginResponse.InvalidCreds:
318318+ valuesUpdate.userPassInvalid = true;
319319+ break;
320320+ case PlatformLoginResponse.Suspended:
321321+ valuesUpdate.accountSuspended = true;
322322+ break;
323323+ case PlatformLoginResponse.TakenDown:
324324+ case PlatformLoginResponse.Deactivated:
325325+ valuesUpdate.accountGone = true;
326326+ break;
327327+ default:
328328+ console.warn(`createViolationForUser was not properly handled for ${violationType}`);
329329+ return false;
330330+ }
331331+332332+ const {success} = await db.insert(violations).values({userId: userId, ...valuesUpdate}).onConflictDoUpdate({target: violations.userId, set: valuesUpdate});
333333+ return success;
334334+};
335335+336336+const getViolationDeleteQueryForUser = (db:DrizzleD1Database, userId: string) => {
337337+ return db.delete(violations).where(and(eq(violations.userId, userId),
338338+ and(ne(violations.tosViolation, true), ne(violations.accountGone, true))));
339339+};
340340+341341+export const clearViolationForUser = async(env: Bindings, userId: string) => {
342342+ const db: DrizzleD1Database = drizzle(env.DB);
343343+ const {success} = await getViolationDeleteQueryForUser(db, userId);
344344+ return success;
345345+};
346346+347347+export const getViolationsForCurrentUser = async(c: Context) => {
348348+ const userData = c.get("user");
349349+ if (userData) {
350350+ const db: DrizzleD1Database = drizzle(c.env.DB);
351351+ return await db.select().from(violations).where(eq(violations.userId, userData.id)).limit(1).run();
352352+ } else {
353353+ return {success: false, results: []};
290354 }
291355};