Mastodon preferences
This commit is contained in:
parent
7b6e3700e4
commit
a5ffbec8d4
8 changed files with 176 additions and 6 deletions
|
|
@ -30,6 +30,9 @@ class User(BaseModel):
|
||||||
mastodon_username = TextField(null=True)
|
mastodon_username = TextField(null=True)
|
||||||
mastodon_access_token = TextField(null=True)
|
mastodon_access_token = TextField(null=True)
|
||||||
|
|
||||||
|
mastodon_attn_list = TextField(null=True)
|
||||||
|
mastodon_post_public = BooleanField(null=True, default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = 'user'
|
table_name = 'user'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ def user_mastodon_user_set(id, mastodon_username, mastodon_access_token):
|
||||||
)
|
)
|
||||||
return q.execute()
|
return q.execute()
|
||||||
|
|
||||||
|
def user_preferences_set(id, mastodon_post_public, mastodon_attn_list):
|
||||||
|
q = User.update(
|
||||||
|
mastodon_attn_list=mastodon_attn_list,
|
||||||
|
mastodon_post_public=mastodon_post_public
|
||||||
|
).where(
|
||||||
|
User.id == id
|
||||||
|
)
|
||||||
|
return q.execute()
|
||||||
|
|
||||||
def mastodon_server_get(name):
|
def mastodon_server_get(name):
|
||||||
return MastodonServer.get(name=name)
|
return MastodonServer.get(name=name)
|
||||||
|
|
||||||
|
|
|
||||||
28
flask/api.py
28
flask/api.py
|
|
@ -1,11 +1,14 @@
|
||||||
|
import re
|
||||||
import requests
|
import requests
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import Blueprint, jsonify, abort, request
|
from flask import Blueprint, jsonify, abort, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
||||||
from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set
|
from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set, user_preferences_set
|
||||||
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
||||||
|
|
||||||
|
RE_MASTODON_ATTN_LIST = re.compile(r'^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$')
|
||||||
|
|
||||||
api = Blueprint('api', __name__)
|
api = Blueprint('api', __name__)
|
||||||
|
|
||||||
@api.route("/me")
|
@api.route("/me")
|
||||||
|
|
@ -17,9 +20,30 @@ def me():
|
||||||
"username": user.telegram_username,
|
"username": user.telegram_username,
|
||||||
"telegram_photo_url": user.telegram_photo_url,
|
"telegram_photo_url": user.telegram_photo_url,
|
||||||
"mastodon_server": user.mastodon_server.name,
|
"mastodon_server": user.mastodon_server.name,
|
||||||
"mastodon_username": user.mastodon_username
|
"mastodon_username": user.mastodon_username,
|
||||||
|
"mastodon_attn_list": user.mastodon_attn_list,
|
||||||
|
"mastodon_post_public": user.mastodon_post_public
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@api.route("/profile", methods=["POST",])
|
||||||
|
@login_required
|
||||||
|
def profile():
|
||||||
|
user = current_user.db_user
|
||||||
|
|
||||||
|
attn_list = request.json['attn_list'] if 'attn_list' in request.json else user.mastodon_attn_list
|
||||||
|
post_public = request.json['post_public'] if 'post_public' in request.json else user.mastodon_post_public
|
||||||
|
|
||||||
|
if attn_list is not None and RE_MASTODON_ATTN_LIST.fullmatch(attn_list) is None:
|
||||||
|
abort(500)
|
||||||
|
|
||||||
|
user_preferences_set(
|
||||||
|
user.id,
|
||||||
|
post_public,
|
||||||
|
attn_list
|
||||||
|
)
|
||||||
|
|
||||||
|
return ('', 204)
|
||||||
|
|
||||||
@api.route('/mastodon_oauth')
|
@api.route('/mastodon_oauth')
|
||||||
@login_required
|
@login_required
|
||||||
def mastodon_oauth():
|
def mastodon_oauth():
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Fieldset,
|
||||||
Flex,
|
Flex,
|
||||||
Modal,
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
|
|
@ -11,8 +13,12 @@ import {
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { NavigateButton } from "./NavigateButton";
|
import { Link, useLoaderData } from "react-router";
|
||||||
import { Link } from "react-router";
|
import { useForm } from "@mantine/form";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { fetchHeaders } from "./fetch";
|
||||||
|
|
||||||
|
const RE_MASTODON_ACCOUNTS = /^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$/;
|
||||||
|
|
||||||
export const AuthorizeMastodonModal: React.FC<{
|
export const AuthorizeMastodonModal: React.FC<{
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
|
|
@ -46,9 +52,21 @@ export const AuthorizeMastodonModal: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MastodonPreferencesForm = {
|
||||||
|
attn_list?: string;
|
||||||
|
post_public?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const profileLoader = async () =>
|
||||||
|
fetch(`/api/me`).then((response) => response.json());
|
||||||
|
|
||||||
export const Profile: React.FC = () => {
|
export const Profile: React.FC = () => {
|
||||||
const { username, telegram_photo_url, mastodon_server, mastodon_username } =
|
const { username, telegram_photo_url, mastodon_server, mastodon_username } =
|
||||||
useUserContext();
|
useUserContext();
|
||||||
|
const { mastodon_attn_list, mastodon_post_public } = useLoaderData<{
|
||||||
|
mastodon_attn_list?: string;
|
||||||
|
mastodon_post_public?: boolean;
|
||||||
|
}>();
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
const mastodon_account = React.useMemo(
|
const mastodon_account = React.useMemo(
|
||||||
|
|
@ -56,6 +74,51 @@ export const Profile: React.FC = () => {
|
||||||
[mastodon_server, mastodon_username],
|
[mastodon_server, mastodon_username],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const form = useForm<MastodonPreferencesForm>({
|
||||||
|
mode: "uncontrolled",
|
||||||
|
initialValues: {
|
||||||
|
attn_list: mastodon_attn_list,
|
||||||
|
post_public: mastodon_post_public,
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
attn_list: (value: string) =>
|
||||||
|
!value || RE_MASTODON_ACCOUNTS.test(value)
|
||||||
|
? null
|
||||||
|
: "Please enter a valid list of accounts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = React.useCallback(
|
||||||
|
form.onSubmit((values) => {
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`/api/profile`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: fetchHeaders(),
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
notifications.show({
|
||||||
|
title: "Success",
|
||||||
|
message: "Your preferences have been saved",
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
title: "Error",
|
||||||
|
message: "There was a problem saving your preferences",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex align="center" gap="md" mb="xl">
|
<Flex align="center" gap="md" mb="xl">
|
||||||
|
|
@ -70,6 +133,24 @@ export const Profile: React.FC = () => {
|
||||||
<TextInput label="Account" w="50%" value={mastodon_account} />
|
<TextInput label="Account" w="50%" value={mastodon_account} />
|
||||||
<Button onClick={open}>Authorize with Mastodon</Button>
|
<Button onClick={open}>Authorize with Mastodon</Button>
|
||||||
<AuthorizeMastodonModal opened={opened} onClose={close} />
|
<AuthorizeMastodonModal opened={opened} onClose={close} />
|
||||||
|
<Fieldset legend="Preferences" my="lg">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Checkbox
|
||||||
|
{...form.getInputProps("post_public", { type: "checkbox" })}
|
||||||
|
label="Post orders publically"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
{...form.getInputProps("attn_list")}
|
||||||
|
label="Tag Accounts"
|
||||||
|
w="50%"
|
||||||
|
description="Additional accounts to tag with your orders"
|
||||||
|
/>
|
||||||
|
<Button type="submit" loading={loading} mt="md">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Fieldset>
|
||||||
</Paper>
|
</Paper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ export interface UserContextData {
|
||||||
telegram_photo_url?: string;
|
telegram_photo_url?: string;
|
||||||
mastodon_server?: string;
|
mastodon_server?: string;
|
||||||
mastodon_username?: string;
|
mastodon_username?: string;
|
||||||
|
mastodon_attn_list?: string;
|
||||||
|
mastodon_post_public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserContext = React.createContext<UserContextData>({});
|
const UserContext = React.createContext<UserContextData>({});
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";
|
||||||
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
|
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
|
||||||
import { UserContextProvider } from "./UserContext";
|
import { UserContextProvider } from "./UserContext";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
import { Profile } from "./Profile";
|
import { Profile, profileLoader } from "./Profile";
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -61,6 +61,7 @@ const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "profile",
|
path: "profile",
|
||||||
Component: Profile,
|
Component: Profile,
|
||||||
|
loader: profileLoader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "orders/:username",
|
path: "orders/:username",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"lib": ["ES2015", "DOM"],
|
"lib": ["ES2018", "DOM"],
|
||||||
"typeRoots": ["src/*.d.ts"],
|
"typeRoots": ["src/*.d.ts"],
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
migrations/016_add_user_mastodon_attn_public.py
Normal file
50
migrations/016_add_user_mastodon_attn_public.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Peewee migrations -- 016_add_user_mastodon_attn_public.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||||
|
> Model = migrator.ModelClass # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
> migrator.add_constraint(model, name, sql)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.drop_constraints(model, *constraints)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
import peewee as pw
|
||||||
|
from peewee_migrate import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
with suppress(ImportError):
|
||||||
|
import playhouse.postgres_ext as pw_pext
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your migrations here."""
|
||||||
|
|
||||||
|
migrator.add_fields(
|
||||||
|
'user',
|
||||||
|
|
||||||
|
mastodon_attn_list=pw.TextField(null=True),
|
||||||
|
mastodon_post_public=pw.BooleanField(default=False, null=True))
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
"""Write your rollback migrations here."""
|
||||||
|
|
||||||
|
migrator.remove_fields('user', 'mastodon_attn_list', 'mastodon_post_public')
|
||||||
Loading…
Reference in a new issue